GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

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. When the motion needs to point at what is right instead of what is wrong, the text highlight animations carry that opposite signal, and turning an error color into light is the job of the CSS glow effects.

01Horizontal shake

What happens on screen

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.

Horizontal shake
Open in a new tab

01 · Horizontal shake — Measured area 0.494% · moving frames 8/23Original by GODRICH · SCSS and React ship in the zip · See the password

translateX@keyframes500ms

The key property

One translateX keyframe with shrinking amplitude does the entire thing, with no animation library involved.

@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

What it looks like

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.

Wiggle rotate
Open in a new tab

02 · Wiggle rotate — Measured area 1.942% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

rotatetransform-origin600ms

What to read in the code

Only the rotate angle changes across the keyframe, so the checkbox shape itself never bends.

.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

Where a css button shake animation fits

A save button that comes back with an error wobbles like jello and settles back down by the end.

Jello wobble
Open in a new tab

03 · Jello wobble — Measured area 1.069% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

skewXskewY800ms

How it is built

skewX and skewY move together inside the same keyframe, distorting the button along two diagonals at once before it calms.

@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

What you see

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.

Error border flash
Open in a new tab

04 · Error border flash — Measured area 0.869% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

opacityinset: 0500ms

How it works

Keeping the field still stops the fields next to it in the row from looking like they shook too.

.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

Where it fits

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.

Input head-shake
Open in a new tab

05 · Input head-shake — Measured area 2.372% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

rotateYperspective900ms

How to wire it up

That 3D tilt only reads correctly once its parent element carries a perspective.

.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

What moves

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.

Attention icon bounce
Open in a new tab

06 · Attention icon bounce — Measured area 0.726% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

scaletranslateY700ms

The core of it

Because the icon only decorates the message, it carries aria-hidden and the real warning stays in the text next to it.

@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

The moment it fires

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.

Shake via :user-invalid
Open in a new tab

07 · Shake via :user-invalid — Measured area 0.507% · moving frames 8/23Original by GODRICH · SCSS and React ship in the zip · See the password

:user-invalidpattern500ms

The idea behind it

An empty required field stays quiet on page load, and only starts shaking after someone types something and clicks away.

.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

What the user notices

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.

One-shot shake reset pattern
Open in a new tab

08 · One-shot shake reset pattern — Measured area 0.56% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

animationendreflowJS 6줄

Why it is done this way

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.

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

Where this shows up

By default, a textarea shakes the same way item 01 does when a message is too short.

Outline fallback for reduced motion
Open in a new tab

09 · Outline fallback for reduced motion — Measured area 0.336% · moving frames 8/23Original by GODRICH · SCSS and React ship in the zip · See the password

prefers-reduced-motionrm__ringopacity

Worth noting

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.

@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?

What goes wrong

Eight of the nine animations above — the rotate-driven css wiggle animation in 02 included — 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.

How to avoid it

Neighboring elements appearing to shift as the shake amplitude grows uses the same arithmetic as spacing the deliberately overlapping layers in the collage maximalist landing kit.

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

What reduced motion leaves

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.

What else to check

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 css error 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. If the page needs the whole success-and-failure vocabulary rather than one shake, the form validation CSS animations are that set.

Enter the archive password

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