GODRICH

9 CSS Shake Animation Examples for Form Errors

Type a wrong password and the box shakes before you read why — that's a css shake animation, code that makes a form say "try again" with no

Auto-plays · click a tile to jump to its section · all nine in one zip

The order below follows where inside a form the failure actually shows up, not popularity. 01 through 03 shake the value someone just typed wrong — an email field, an unchecked box, a button that failed to save. 04 backs off to a quieter signal that flashes instead of moving. 05 and 06 cover heavier failures: a mismatched password, an icon meant to catch the eye on its own. 07 hands the whole job to the browser with no JavaScript at all, 08 deals with the same field failing twice in a row, and 09 makes sure someone who has turned off motion still gets the same warning.

01Horizontal shake

Leave an email field holding a bad address, and the whole input shakes left and right four times before settling while its border turns red. One translateX keyframe with shrinking amplitude does the entire thing, with no animation library involved.

translateX@keyframes500ms
@keyframes sh-shake {
  0%, 22%, 100% { transform: translateX(0); }
  2%  { transform: translateX(-7px); }
  4%  { transform: translateX(7px); }
  6%  { transform: translateX(-7px); }
  8%  { transform: translateX(7px); }
  10% { transform: translateX(-5px); }
  12% { transform: translateX(5px); }
  14% { transform: translateX(-3px); }
  16% { transform: translateX(3px); }
  18% { transform: translateX(-1px); }
  20% { transform: translateX(1px); }
}
.sh__input.sh--invalid { animation: sh-shake $duration $easing; }

02Wiggle rotate

Try to submit a form without checking the terms box, and the whole checkbox row tips left and right as if it's shaking its head. Only the rotate angle changes across the keyframe, so the checkbox shape itself never bends.

rotatetransform-origin600ms
.wg__row { transform-origin: left center; }

@keyframes wg-wiggle {
  0%, 100% { transform: rotate(0); }
  15% { transform: rotate(-4deg); }
  30% { transform: rotate(4deg); }
  45% { transform: rotate(-3deg); }
  60% { transform: rotate(3deg); }
  75% { transform: rotate(-1.5deg); }
  90% { transform: rotate(1.5deg); }
}
.wg__row.wg--wiggle { animation: wg-wiggle $duration $easing; }

03Jello wobble

A save button that comes back with an error wobbles like jello and settles back down by the end. skewX and skewY move together inside the same keyframe, distorting the button along two diagonals at once before it calms.

skewXskewY800ms
@keyframes je-jello {
  0%, 100% { transform: none; }
  15% { transform: skewX(-9deg) skewY(-9deg); }
  30% { transform: skewX(7deg) skewY(7deg); }
  45% { transform: skewX(-5deg) skewY(-5deg); }
  60% { transform: skewX(3deg) skewY(3deg); }
  75% { transform: skewX(-2deg) skewY(-2deg); }
  90% { transform: skewX(1deg) skewY(1deg); }
}
.je__btn.je--jello { animation: je-jello $duration $easing; }

04Error border flash

A card-number field never moves at all — instead, a red ring layered behind it flashes twice through opacity while the input itself stays perfectly still. Keeping the field still stops the fields next to it in the row from looking like they shook too.

opacityinset: 0500ms
.bf__ring {
  position: absolute;
  inset: -4px;
  border: 2px solid $color;
  opacity: 0;
}

@keyframes bf-flash {
  0%, 100% { opacity: 0; }
  15% { opacity: 1; }
  30% { opacity: 0; }
  45% { opacity: 1; }
  60% { opacity: 0; }
}
.bf__ring.bf--flash { animation: bf-flash $duration $easing; }

05Input head-shake

When a confirm-password field doesn't match, the input tilts in 3D like someone shaking their head no, mixing translateX and rotateY inside one keyframe. That 3D tilt only reads correctly once its parent element carries a perspective.

rotateYperspective900ms
.hs { perspective: 500px; }

@keyframes hs-shake {
  0% { transform: translateX(0); }
  6.5% { transform: translateX(-6px) rotateY(-9deg); }
  18.5% { transform: translateX(5px) rotateY(7deg); }
  31.5% { transform: translateX(-3px) rotateY(-5deg); }
  43.5% { transform: translateX(2px) rotateY(3deg); }
  50%, 100% { transform: translateX(0); }
}
.hs__input.hs--shake { animation: hs-shake $duration $easing; }

06Attention icon bounce

A round warning icon hops up and lands beside a form error, mixing scale and translateY in the same keyframe so the motion reads as springy rather than mechanical. Because the icon only decorates the message, it carries aria-hidden and the real warning stays in the text next to it.

scaletranslateY700ms
@keyframes ib-bounce {
  0%, 100% { transform: translateY(0) scale(1); }
  20% { transform: translateY(-10px) scale(1.15); }
  40% { transform: translateY(0) scale(.95); }
  55% { transform: translateY(-4px) scale(1.05); }
  70% { transform: translateY(0) scale(1); }
}
.ib__icon.ib--bounce { animation: ib-bounce $duration $easing; }

07Shake via :user-invalid

With zero lines of JavaScript, the input:user-invalid pseudo-class shakes an email field the moment the browser marks a touched value invalid, never before. An empty required field stays quiet on page load, and only starts shaking after someone types something and clicks away.

:user-invalidpattern500ms
.ui__input:user-invalid {
  border-color: $color;
  animation: ui-shake $duration $easing;
}

@keyframes ui-shake {
  0%, 22%, 100% { transform: translateX(0); }
  2%  { transform: translateX(-7px); }
  4%  { transform: translateX(7px); }
  6%  { transform: translateX(-7px); }
  8%  { transform: translateX(7px); }
  10% { transform: translateX(-5px); }
  12% { transform: translateX(5px); }
  14% { transform: translateX(-3px); }
  16% { transform: translateX(3px); }
}

08One-shot shake reset pattern

Press "check again" after a wrong password twice in a row, and the field restarts its shake from zero both times, not just the first. Re-adding the same class right after removing it doesn't replay a keyframe that already finished, so a few lines of JavaScript force a reflow in between to reset it.

animationendreflowJS 6줄
var input = document.getElementById('rs-pw');
document.getElementById('rs-check').addEventListener('pointerdown', function () {
  input.classList.remove('rs--shake');
  void input.offsetWidth;
  input.classList.add('rs--shake');
});

09Outline fallback for reduced motion

By default, a textarea shakes the same way item 01 does when a message is too short. Switch on reduced motion at the system level, though, and the shake swaps for a red ring pulsing through opacity twice instead, so turning off motion doesn't also turn off the warning.

prefers-reduced-motionoutlineopacity
@keyframes rm-ring-pulse {
  0%, 100% { opacity: 0; }
  25% { opacity: 1; }
  50% { opacity: 0; }
  75% { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .rm__textarea.rm--invalid { animation: none !important; }
  .rm__ring.rm--invalid { animation: rm-ring-pulse $dur-loop ease-in-out; }
}

Where does the shake break down?

Eight of the nine animations above share one quirk: once a CSS animation finishes, re-adding the same trigger class doesn't start it over, so a second identical failure produces no shake at all. Item 08's remove-reflow-add sequence is the fix for that, and the same three steps carry over to any of the other seven that need it. The one exception is 07's :user-invalid, which recomputes on every value change and needs no workaround at all. The full vanilla and React source for all nine opens with 3v34eb5k, typed exactly as it reads here with nothing added or left out.

Accessibility

01 through 09 all drop their animation to none under prefers-reduced-motion: reduce, but 04 and 09 don't stop there — since motion is their only signal, both swap in an opacity pulse so the error still shows up once shaking is off the table. Browser support for :user-invalid covers Chrome/Edge 119+, Firefox 88+, and Safari 16.4+ per MDN; older browsers simply never match the selector, so a payment or signup form that can't afford a missed error should lean on the class-based pattern from 01 or 05 instead. A wider set of always-on keyframe classes, one of them a plain shake, lives in 9 CSS keyframe animations, and everything else this site has published sits in the CSS category hub.

FAQ

Why do shake animations use transform instead of margin or left?

transform and opacity skip layout entirely — the browser repaints pixels it already has instead of recalculating position for the shaken element and everything sitting next to it. Animating margin or left recalculates layout on every frame, which runs slower and can nudge neighboring fields along with the one that's actually wrong.

What's the difference between :invalid and :user-invalid?

:invalid matches the moment a page loads if a field's value fails validation, even a field nobody has touched. :user-invalid only matches after someone has typed into that field and moved on, so an empty required field doesn't turn red before anyone had a chance to fill it in.

How do I make a shake animation replay on a second identical error?

Remove the trigger class, force a reflow by reading a property like offsetWidth, then add the class back — the pattern in item 08 — and the browser treats it as a brand-new animation instead of an already-finished one. In React, swapping the element's key remounts it from scratch instead and gets the same restart without touching the DOM directly.

Enter the archive password

The password is inside this article. You will find it as you read.