Error Shake Feedback
Three one-shot validation feedback patterns paired with aria-live hints — a canonical horizontal jitter, a motion-free border-pulse halo for reduced-motion users, and a vertical drop-bounce on the cross axis.
Three one-shot validation feedback patterns paired with aria-live hints — a canonical horizontal jitter, a motion-free border-pulse halo for reduced-motion users, and a vertical drop-bounce on the cross axis.
Feedback / validation / reduced-motion aware
Three one-shot validation feedback patterns — a canonical horizontal jitter, a motion-free border pulse for reduced-motion users, and a vertical bump on the cross axis. All pair with an aria-live hint so the message reads with or without motion.
Canonical translateX shake · invalid on submit
A short horizontal back-and-forth jitter — the canonical "this is wrong" transform. Amplitude stays modest so the motion reads as a correction nudge.
Motion-free box-shadow flash · a11y-safe
No transform — the field stays still while a red box-shadow halo pulses outward twice. Conveys "invalid" with zero motion, so reduced-motion users get full feedback parity.
translateY drop-bounce · cross-axis variant
A short upward dip followed by a downward bounce on the Y axis. The cross-axis motion reads as a head-shake "no, look again" — useful when horizontal jitter is already in use elsewhere on the screen.
An error shake feedback pattern plays a short transform jitter once, pairs it with aria-invalid text, and swaps to a static outline for reduced motion.
1<label class="error-shake-demo"><span>Email</span><span class="error-shake-demo__field" data-state="shaking">4<input value="name@" aria-invalid="true" aria-describedby="email-error"></span>6<span id="email-error" class="error-shake-demo__hint" role="status">Email looks invalid. Include an @ and domain.</span></label><style>.error-shake-demo {display: grid;gap: .45rem;width: min(100%, 22rem);color: #e2e8f0;font: 600 .9rem/1.25 ui-sans-serif, system-ui;}.error-shake-demo__field {display: block;border: 1px solid rgba(248, 113, 113, .7);border-radius: 10px;background: rgba(15, 23, 42, .72);}25.error-shake-demo__field[data-state="shaking"] {26animation: error-shake-recipe-jitter 360ms cubic-bezier(.36,.07,.19,.97) both;}.error-shake-demo input {width: 100%;border: 0;padding: .65rem .75rem;color: #f8fafc;background: transparent;font: inherit;}.error-shake-demo__hint {min-block-size: 1.2em;color: #f87171;font-size: .78rem;}41@keyframes error-shake-recipe-jitter {10%, 90% { transform: translateX(-2px); }20%, 80% { transform: translateX(5px); }30%, 50%, 70% { transform: translateX(-8px); }40%, 60% { transform: translateX(8px); }}@media (prefers-reduced-motion: reduce) {.error-shake-demo__field[data-state="shaking"] {49animation: none;outline: 2px solid #f87171;outline-offset: 2px;}}</style>
No. Pair the motion with aria-invalid, connected error text, and a visible color or outline state. Motion should reinforce the failure, not be the only signal.
No. Pair the motion with aria-invalid, connected error text, and a visible color or outline state. Motion should reinforce the failure, not be the only signal.
Looping the shake keeps shouting after the user already read the error; firing it once per failed submit and resting lets the static error state carry the rest.
No. Play the shake once when validation fails, then leave the field in a stable invalid state. A looping shake makes the form harder to read and keeps drawing attention after the user already knows what changed.
Use small horizontal offsets and a short duration. Large offsets can feel punitive, especially on dense forms where the field may collide visually with neighboring controls.
Replace the shake with a static invalid style such as an outline, border, or message update. The form must remain clear even when animation is disabled.
Both look like the same shake — but left changes the layout box every frame (cells around it can shove) while transform stays on the composite layer.
Transform moves the painted field without changing document flow. Animating left, margin, or padding can trigger layout work and may shove nearby content during the error state.
Same 1.6s cycle, same translateX one-shot, same ±10px peak amplitude — only the amplitude curve through the keyframe stops differs. BEFORE keeps the burst at ±8/±8/±8 the whole way through, so the field hits the same wall every beat (a hard buzz). AFTER decays the amplitude on each successive peak (±10 → ±8 → ±6 → ±4 → ±3 → ±2 → 0), so the field clearly loses energy and settles into rest like a real spring. Reads less like a computer alarm, more like a physical correction.
The base shake uses equal-amplitude translateX bursts (±2px → ±5px → ±8px → ±8px → ±5px → ±2px). It reads correctly, but every peak hits the same hard wall — closer to a computer jitter than a real spring releasing energy. Real-world physics: a pushed object oscillates with decaying amplitude as friction bleeds energy away. Same data-state trigger, same translateX property, same 360ms duration — what changes is the amplitude curve at each stop: the first peaks are large, each subsequent peak smaller, and the field eases to rest. Reads more like a spring snapping back than a hard buzz.
Append these rules inside the <style> block from the base snippet above.
/* Advanced: spring-decay shake — extends the base recipe.Same data-state="shaking" trigger, same one-shot animation, sametranslateX property. The keyframe amplitudes now decay through thecycle (10px → 8px → 6px → 4px → 2px → 0) so the field settlesinto rest instead of slamming at full strength all the way through.The cubic-bezier easing is replaced by a softer ease-out so eachsub-peak loses a bit more energy on the way out. */.error-shake-demo__field[data-state="shaking"] {animation: error-shake-recipe-decay 460ms cubic-bezier(.22, .68, .36, 1) both;}@keyframes error-shake-recipe-decay {0% { transform: translateX(0); }12% { transform: translateX(-10px); }24% { transform: translateX(8px); }36% { transform: translateX(-6px); }50% { transform: translateX(4px); }64% { transform: translateX(-3px); }78% { transform: translateX(2px); }92% { transform: translateX(-1px); }100% { transform: translateX(0); }}@media (prefers-reduced-motion: reduce) {.error-shake-demo__field[data-state="shaking"] {animation: none;outline: 2px solid #f87171;outline-offset: 2px;}}
Form-validation error shake is a tiny keyframe animation (translate-X back and forth a few pixels) triggered when an input fails validation. The pattern adds the .is-shaking class on submit, force-reflows the element with void el.offsetWidth, then re-adds the class so the keyframe restarts even on rapid resubmits. An aria-live="polite" region announces the validation message in parallel.
Reach for shake feedback on inline form validation, permission-denied taps, “wrong password” affordances — any rejection that the user needs to notice without leaving their current context. Skip it for any error that requires an action beyond “fix the input” (network errors, server errors); those need a toast or a modal with explicit next steps.
A 5-frame @keyframes rule named shake translates the element along the X axis with diminishing amplitude (e.g. 0% / 25% / 50% / 75% / 100% at 0px / -8px / 6px / -4px / 0px) over ~320ms. Adding the class kicks it off; the trick is restarting it on the same element — CSS animations do not replay when re-applied to a node that already has the class. The canonical fix: remove the class, read void el.offsetWidth to force a synchronous reflow, then re-add the class. The browser now sees a fresh animation start and the keyframe replays from frame 0.
Forgetting the reflow trick is the most common bug — the user mashes submit, the error message updates, but the input never shakes again. Tying the shake to animationend with class removal is fine, but racing it against rapid resubmits means the class can be removed before the next add triggers reflow; use requestAnimationFrame bracketing if shakes feel inconsistent. Keep the amplitude under ~8px — large translations are uncomfortable on tightly-packed forms and can collide with adjacent layout.
Shake is a decorative reinforcement — the error announcement itself must come from aria-live="polite" or role="alert" with the actual validation message. Under prefers-reduced-motion: reduce swap the translate keyframe for a brief border-color flash (red 80% opacity for 200ms then back) so the error still reads without the lateral motion that can trigger vestibular disorders.
Shake feedback should be a secondary cue after a clear error message. The animation can draw the eye to the invalid field, but text and aria-describedby need to explain what the user must fix.
Keep the distance small and the duration short. Large shake motion feels punitive and can be uncomfortable; reduced motion should use color, border, and message changes without lateral movement.