16 CSS Bounce Animations14 / 16

CSS + JSMIT licensed

Form Error Alert Bounce

A form field that shakes horizontally when validation fails — the Stripe checkout / Google form field / login form error pattern. Sharp aggressive shake reads as "this needs your attention NOW" without being ambiguous. Muted rose surface with a card email + payment field, red border on the invalid field, and error copy below. Users search for this when building form validation, checkout flows, or any input that needs failure feedback. Pure horizontal translateX shake — never a diagonal or scale change.

Published

Live Demo
Try it

The code

<div class="bn-14">
  <p class="bn-14__label">Form validation error shake</p>
  <form class="bn-14__form" id="bn-14-form" onsubmit="event.preventDefault()" novalidate>
    <label class="bn-14__field">
      <span>Email address</span>
      <input type="email" value="alex@studio" autocomplete="email" class="bn-14__input bn-14__input--email" />
    </label>
    <label class="bn-14__field">
      <span>Card number</span>
      <input type="text" value="4242 4242 4242 4242" inputmode="numeric" class="bn-14__input bn-14__input--card" id="bn-14-card" aria-invalid="false" aria-describedby="bn-14-err" />
      <span class="bn-14__error" id="bn-14-err" role="alert"></span>
    </label>
    <button type="submit" class="bn-14__submit" id="bn-14-submit">Pay $189</button>
  </form>
  <p class="bn-14__hint">The invalid field shakes horizontally; error copy fades in below.</p>
</div>
.bn-14 {
  --page-bg: #fef2f2;
  --surface: #ffffff;
  --ink: #450a0a;
  --sub: #9f7d7d;
  --brand: #dc2626;
  --brand-dark: #991b1b;
  --good: #065f46;
  --line: #fecaca;
  --input: #fafafa;
  font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 20px;
  padding: 32px;
  background: radial-gradient(70% 60% at 50% 40%, rgba(220,38,38,.06) 0%, transparent 60%), var(--page-bg);
  color: var(--ink);
  text-align: center;
}

.bn-14 *,
.bn-14 *::before,
.bn-14 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.bn-14 ::selection {
  background: var(--brand);
  color: #fff;
}

.bn-14__label {
  font-size: .9rem;
  font-weight: 600;
  color: var(--sub);
  letter-spacing: .02em;
}

.bn-14__form {
  display: flex;
  flex-direction: column;
  gap: 14px;
  width: min(380px,100%);
  padding: 26px 24px;
  border-radius: 14px;
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: 0 20px 40px -12px rgba(220,38,38,.15);
  text-align: left;
}

.bn-14__field {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.bn-14__field > span:first-child {
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--sub);
}

.bn-14__input {
  padding: 11px 14px;
  border: 1.5px solid #e5e7eb;
  border-radius: 8px;
  background: var(--input);
  color: var(--ink);
  font-family: inherit;
  font-size: .9rem;
  transition: border-color .18s ease,background .18s ease;
  will-change: transform;
}

.bn-14__input:focus {
  outline: none;
  border-color: var(--good);
  background: var(--surface);
}

.bn-14__input--card.is-error {
  border-color: var(--brand);
  background: #fef7f7;
}

.bn-14__input--card.is-shaking {
  animation: bn-14-shake .4s ease-out;
}

@keyframes bn-14-shake {
  0% {
    transform: translateX(0);
  }

  20% {
    transform: translateX(-8px);
  }

  40% {
    transform: translateX(8px);
  }

  60% {
    transform: translateX(-6px);
  }

  80% {
    transform: translateX(4px);
  }

  100% {
    transform: translateX(0);
  }
}

.bn-14__error {
  display: none;
  margin-top: 4px;
  font-size: .75rem;
  font-weight: 600;
  color: var(--brand);
  line-height: 1.4;
}

.bn-14__error.is-visible {
  display: block;
  animation: bn-14-error-in .3s ease-out .2s both;
}

@keyframes bn-14-error-in {
  from {
    opacity: 0;
    transform: translateY(-4px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.bn-14__submit {
  padding: 12px 18px;
  border: none;
  border-radius: 8px;
  background: var(--good);
  color: #fff;
  font-family: inherit;
  font-size: .92rem;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 8px 18px -6px rgba(6,95,70,.4);
  transition: background .18s ease,transform .12s ease;
  margin-top: 2px;
}

.bn-14__submit:hover {
  background: #047857;
  transform: translateY(-1px);
}

.bn-14__submit:focus-visible {
  outline: 2px solid var(--good);
  outline-offset: 3px;
}

.bn-14__hint {
  font-size: .78rem;
  color: var(--sub);
  max-width: 380px;
  line-height: 1.5;
}

@media (prefers-reduced-motion: reduce) {
  .bn-14__input--card.is-shaking {
    animation: none!important;
    transform: none!important;
  }

  .bn-14__error.is-visible {
    animation: none!important;
    opacity: 1!important;
    transform: none!important;
  }
}
(function(){
  var root = document.querySelector('.bn-14');
  if (!root) return;
  var card = root.querySelector('#bn-14-card');
  var err = root.querySelector('#bn-14-err');
  var btn = root.querySelector('#bn-14-submit');
  var form = root.querySelector('#bn-14-form');
  if (!card || !err || !btn) return;
  var errors = [
    'Card number is invalid. Check the digits and try again.',
    'This card was declined. Try a different card.',
    'Your card has expired. Please use a valid card.'
  ];
  var i = 0;
  function trigger(){
    card.classList.remove('is-shaking','is-error');
    err.classList.remove('is-visible');
    void card.offsetHeight;
    card.classList.add('is-shaking','is-error');
    card.setAttribute('aria-invalid','true');
    err.textContent = errors[i % errors.length];
    err.classList.add('is-visible');
    i++;
    setTimeout(function(){
      card.classList.remove('is-shaking');
    }, 500);
  }
  btn.addEventListener('click', function(e){ e.preventDefault(); trigger(); });
  form.addEventListener('submit', function(e){ e.preventDefault(); trigger(); });
  // Auto-cycle so gallery + preview show the pattern
  setInterval(trigger, 3000);
  setTimeout(trigger, 500);
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Bounce Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Form Error Alert Bounce
Source: https://codefronts.com/motion/css-bounce-animation/form-error-alert-bounce/

A form field that shakes horizontally when validation fails — the Stripe checkout / Google form field / login form error pattern. Sharp aggressive shake reads as "this needs your attention NOW" without being ambiguous. Muted rose surface with a card email + payment field, red border on the invalid field, and error copy below. Users search for this when building form validation, checkout flows, or any input that needs failure feedback. Pure horizontal translateX shake — never a diagonal or scale change.
## HTML
```html
<div class="bn-14">
  <p class="bn-14__label">Form validation error shake</p>
  <form class="bn-14__form" id="bn-14-form" onsubmit="event.preventDefault()" novalidate>
    <label class="bn-14__field">
      <span>Email address</span>
      <input type="email" value="alex@studio" autocomplete="email" class="bn-14__input bn-14__input--email" />
    </label>
    <label class="bn-14__field">
      <span>Card number</span>
      <input type="text" value="4242 4242 4242 4242" inputmode="numeric" class="bn-14__input bn-14__input--card" id="bn-14-card" aria-invalid="false" aria-describedby="bn-14-err" />
      <span class="bn-14__error" id="bn-14-err" role="alert"></span>
    </label>
    <button type="submit" class="bn-14__submit" id="bn-14-submit">Pay $189</button>
  </form>
  <p class="bn-14__hint">The invalid field shakes horizontally; error copy fades in below.</p>
</div>
```
## CSS
```css
.bn-14 {
  --page-bg: #fef2f2;
  --surface: #ffffff;
  --ink: #450a0a;
  --sub: #9f7d7d;
  --brand: #dc2626;
  --brand-dark: #991b1b;
  --good: #065f46;
  --line: #fecaca;
  --input: #fafafa;
  font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 20px;
  padding: 32px;
  background: radial-gradient(70% 60% at 50% 40%, rgba(220,38,38,.06) 0%, transparent 60%), var(--page-bg);
  color: var(--ink);
  text-align: center;
}

.bn-14 *,
.bn-14 *::before,
.bn-14 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.bn-14 ::selection {
  background: var(--brand);
  color: #fff;
}

.bn-14__label {
  font-size: .9rem;
  font-weight: 600;
  color: var(--sub);
  letter-spacing: .02em;
}

.bn-14__form {
  display: flex;
  flex-direction: column;
  gap: 14px;
  width: min(380px,100%);
  padding: 26px 24px;
  border-radius: 14px;
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: 0 20px 40px -12px rgba(220,38,38,.15);
  text-align: left;
}

.bn-14__field {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.bn-14__field > span:first-child {
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--sub);
}

.bn-14__input {
  padding: 11px 14px;
  border: 1.5px solid #e5e7eb;
  border-radius: 8px;
  background: var(--input);
  color: var(--ink);
  font-family: inherit;
  font-size: .9rem;
  transition: border-color .18s ease,background .18s ease;
  will-change: transform;
}

.bn-14__input:focus {
  outline: none;
  border-color: var(--good);
  background: var(--surface);
}

.bn-14__input--card.is-error {
  border-color: var(--brand);
  background: #fef7f7;
}

.bn-14__input--card.is-shaking {
  animation: bn-14-shake .4s ease-out;
}

@keyframes bn-14-shake {
  0% {
    transform: translateX(0);
  }

  20% {
    transform: translateX(-8px);
  }

  40% {
    transform: translateX(8px);
  }

  60% {
    transform: translateX(-6px);
  }

  80% {
    transform: translateX(4px);
  }

  100% {
    transform: translateX(0);
  }
}

.bn-14__error {
  display: none;
  margin-top: 4px;
  font-size: .75rem;
  font-weight: 600;
  color: var(--brand);
  line-height: 1.4;
}

.bn-14__error.is-visible {
  display: block;
  animation: bn-14-error-in .3s ease-out .2s both;
}

@keyframes bn-14-error-in {
  from {
    opacity: 0;
    transform: translateY(-4px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.bn-14__submit {
  padding: 12px 18px;
  border: none;
  border-radius: 8px;
  background: var(--good);
  color: #fff;
  font-family: inherit;
  font-size: .92rem;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 8px 18px -6px rgba(6,95,70,.4);
  transition: background .18s ease,transform .12s ease;
  margin-top: 2px;
}

.bn-14__submit:hover {
  background: #047857;
  transform: translateY(-1px);
}

.bn-14__submit:focus-visible {
  outline: 2px solid var(--good);
  outline-offset: 3px;
}

.bn-14__hint {
  font-size: .78rem;
  color: var(--sub);
  max-width: 380px;
  line-height: 1.5;
}

@media (prefers-reduced-motion: reduce) {
  .bn-14__input--card.is-shaking {
    animation: none!important;
    transform: none!important;
  }

  .bn-14__error.is-visible {
    animation: none!important;
    opacity: 1!important;
    transform: none!important;
  }
}
```

## JavaScript
```js
(function(){
  var root = document.querySelector('.bn-14');
  if (!root) return;
  var card = root.querySelector('#bn-14-card');
  var err = root.querySelector('#bn-14-err');
  var btn = root.querySelector('#bn-14-submit');
  var form = root.querySelector('#bn-14-form');
  if (!card || !err || !btn) return;
  var errors = [
    'Card number is invalid. Check the digits and try again.',
    'This card was declined. Try a different card.',
    'Your card has expired. Please use a valid card.'
  ];
  var i = 0;
  function trigger(){
    card.classList.remove('is-shaking','is-error');
    err.classList.remove('is-visible');
    void card.offsetHeight;
    card.classList.add('is-shaking','is-error');
    card.setAttribute('aria-invalid','true');
    err.textContent = errors[i % errors.length];
    err.classList.add('is-visible');
    i++;
    setTimeout(function(){
      card.classList.remove('is-shaking');
    }, 500);
  }
  btn.addEventListener('click', function(e){ e.preventDefault(); trigger(); });
  form.addEventListener('submit', function(e){ e.preventDefault(); trigger(); });
  // Auto-cycle so gallery + preview show the pattern
  setInterval(trigger, 3000);
  setTimeout(trigger, 500);
})();
```

How this works

The invalid input field animates via @keyframes bn-14-shake — a 6-stop cascade of translateX that oscillates: 0% translateX(0) → 20% translateX(-8px) → 40% translateX(8px) → 60% translateX(-6px) → 80% translateX(4px) → 100% translateX(0). The decreasing amplitude (8 → 8 → 6 → 4 → 0) mimics a decaying spring — the field "vibrates" then settles. Duration 400ms with ease-out (fast start, slow end) so the shake feels aggressive at the beginning and calm by the end.

The field's border simultaneously flashes red via border-color transition — the color change is INSTANT (no smooth transition), so users see the shake and the red border in the same moment. The instant color change reinforces the shake's urgency; a smooth color transition would soften the alert.

Below the field, an error message fades in with a slight downward drift via @keyframes bn-14-error-in — subtle transform + opacity so the copy "arrives" after the shake completes. This 200ms delay gives users a moment to see the shake before the copy demands reading.

The demo shows a compact form (email + card + error) centered as the hero. JS auto-triggers the error state every 3 seconds so the gallery + preview show the shake in action. In production, the shake fires on server-side validation failure (400 response) or client-side validation on blur.

Make it yours

  • Change shake intensity by editing the peak amplitude ±8px±4px is subtle (form-hint level), ±12px is aggressive (payment-failure level). Match to error severity.
  • Change shake speed by editing animation-duration: 400ms. Faster (250ms) reads as MORE urgent; slower (600ms) reads as "there's a problem" without alarm. Under 200ms feels glitchy.
  • For color-blind users, DO NOT rely on red border alone. Pair the shake + red border with an icon (⚠) + error text. Users with red-green color blindness (8% of males) can't distinguish red borders.
  • For repeated validation errors, do NOT re-shake on every keystroke — users find that fatiguing. Debounce: only shake on submit or on blur (input loses focus). Or throttle: max one shake per 1.5 seconds.
  • For Tailwind: animate-[bn14_.4s_ease-out] on the input when invalid + red border via border-red-500. Register keyframes in tailwind.config.js. See FAQ #4.

Gotchas — read before shipping

  • The shake MUST be horizontal only — no vertical translate, no rotation, no scale. Vertical shake reads as "this element is falling" (wrong), rotation reads as "broken" (wrong), scale reads as "emphasis" (wrong). Horizontal shake specifically signals "NO, try again."
  • For accessibility, the shake alone is NOT enough to convey error — screen-reader users can't see it. Pair with aria-invalid="true" on the input + role="alert" or aria-live="assertive" on the error message region so the announcement fires immediately.
  • Use role="alert" (which is aria-live="assertive") only for HARD errors that need immediate attention (payment failed, wrong password). Use aria-live="polite" for soft errors (format warnings, hint text).
  • For prefers-reduced-motion, replace the horizontal shake with a solid red border pulse (border color: red instant → dark red 200ms → red) OR just an instant border color change. The information (error) MUST persist — decorative shake removed but state signal preserved.
  • Do NOT auto-clear the error state on the next keystroke — users need to see the error message stay visible while they correct the input. Clear on successful validation (re-submit or blur with valid content), not on every character typed.
  • For repeated validation failures (attempted submit → shake → correct → attempted submit → shake again), ensure each shake fires cleanly. If the shake class isn't removed and re-added between triggers, the animation won't restart. Toggle the class off + on with a void element.offsetHeight in between to force reflow.
  • For touch devices, ensure the error copy is placed BELOW the input (not to the side) — the on-screen keyboard covers the bottom half of the viewport, so error text below the input can get hidden. Test on real iOS and Android.

Browser support

ChromeSafariFirefoxEdge
45+10+40+45+

transform + @keyframes + class toggle. Universal, GPU-accelerated.

Techniques used in this demo

Search CodeFronts

Loading…