Interaction blueprint
Inline validation shake
A short horizontal nudge calls attention once, while the message explains the problem.
Build it
Choose a target and adapt the source to your interface.
<form class="validation-form" novalidate>
<label for="email">Email</label
><input
id="email"
name="email"
type="email"
autocomplete="email"
required
aria-describedby="email-error"
/>
<p id="email-error" class="validation-message" role="alert"></p>
<button type="submit">
Check email<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M5 12h14m-5-5 5 5-5 5" />
</svg>
</button>
<p class="validation-status" role="status"></p>
</form>
<script>
const form = document.querySelector('.validation-form');
const input = form.querySelector('input');
const error = form.querySelector('.validation-message');
const status = form.querySelector('.validation-status');
let animation;
input.addEventListener('input', () => {
input.removeAttribute('aria-invalid');
error.textContent = '';
status.textContent = '';
});
form.addEventListener('submit', (event) => {
event.preventDefault();
animation?.cancel();
const valid = input.validity.valid;
input.setAttribute('aria-invalid', String(!valid));
error.textContent = valid ? '' : 'Enter a valid email address.';
status.textContent = valid ? 'Email format accepted.' : '';
if (!valid) {
input.focus();
if (!matchMedia('(prefers-reduced-motion:reduce)').matches)
animation = input.animate(
[0, -6, 5, -3, 2, 0].map((x) => ({
transform: 'translateX(' + x + 'px)',
})),
{ duration: 320 },
);
}
});
</script>
<style>
.validation-form {
display: grid;
gap: 8px;
max-width: 320px;
font: 14px system-ui;
color: var(--stage-ink, var(--ink, #252b34));
}
.validation-form input {
min-height: 44px;
padding: 10px 12px;
box-sizing: border-box;
}
.validation-form button {
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 28px;
justify-self: start;
min-height: 44px;
padding: 0 16px 0 18px;
border: 0;
border-radius: 999px;
background: var(--stage-ink, var(--ink, #252b34));
color: var(--stage-bg, var(--bg, #f8f9fb));
font: 500 13px/1 system-ui;
cursor: pointer;
}
.validation-form button svg {
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 1.6;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
}
.validation-form button:hover svg {
transform: translateX(3px);
}
.validation-form button:active svg {
transform: translateX(1px);
}
.validation-form button:focus-visible {
outline: 2px solid var(--stage-link, var(--brand-deep, #175bc5));
outline-offset: 3px;
}
.validation-form input {
border: 1px solid #68778c;
border-radius: 4px;
}
.validation-form input[aria-invalid='true'] {
border-color: #b42318;
}
.validation-message {
min-height: 20px;
margin: 0;
color: #b42318;
}
@media (prefers-reduced-motion: reduce) {
.validation-form input {
animation: none;
}
.validation-form button svg {
transition: none;
}
}
</style>Animation breakdown
Frame phases, motion intent, implementation constraints and accessible alternatives.
01
Rest: invalid field and error text.
02
0–220ms: field moves -6, +5, -3, +2px.
03
220–320ms: return to centre; error remains.
Motion intent
A short horizontal nudge calls attention once, while the message explains the problem.
This duration leaves enough time to read the state change while keeping the interface responsive.
The fast acceleration and long ease-out make the state feel responsive while giving the final pixels time to settle.
Motion specification
- Trigger
- A submitted field fails validation.
- Timing
- 320ms
- Easing
- cubic-bezier(.36, .07, .19, .97)
Animated properties
transformopacity
Implementation notes
- Keep invalid and announced aligned to the same visual anchor.
- Drive the transition from semantic state, not a timer, when the trigger is interactive.
- Use transform, opacity and avoid layout work inside the animation loop.
Reduced motion
Swap to the final state instantly, preserving colour, label, and accessible state.
- Use a minimum 44 by 44 pixel interactive target.
- Expose the invalid and announced states with aria-pressed, aria-expanded, or an updated accessible label.
- Keep visible keyboard focus independent from the animation.
Avoid
- Do not add bounce unless a real object pickup or spring settle is part of the interaction.
- Do not hide the final state from assistive technology.
- Do not make the user wait for animation before the underlying state updates.