Animating Form Validation Feedback Accessibly
Part of Focus & State Motion Accessibility in Accessible Motion Architecture.
The problem
A checkout form animates its error messages: each message slides down under its field, the field shakes, and the border turns red with a transition. Users who rely on screen readers hear nothing when this happens; the messages are announced only if they happen to move focus to the field afterwards. One user reports that the shake makes them feel unwell. Another, using speech recognition, cannot read the message because it disappears again after three seconds.
The animation is the visible part of the feedback. The accessible part has to be built separately, and the animation must not get in its way.
Root cause analysis: three channels, one event
A validation error needs to reach users through three independent channels.
The visual channel is the message appearing, the border colour and any motion. This is what the animation covers.
The programmatic channel is the relationship between the field and its message: aria-describedby pointing at the message element, and aria-invalid="true" on the field. When a screen reader user moves to the field, the message is read as part of the field’s description. This works whether or not anything animated.
The announcement channel is a live region for messages that appear without the user moving focus, such as a summary after submit. Live regions announce changes, so a message inserted into a polite live region is read when the user is idle. Announcing every keystroke’s validation is noise; announcing a submission summary is useful.
Two animation details interfere with these channels if handled carelessly. Content inserted and animated at the same time can be announced before it is complete, or not at all, if the insertion and the text update happen in separate frames — insert the message with its text already set. And messages that disappear on a timer cannot be read by people who take longer, which is a WCAG timing problem as well as an animation one.
The shake itself needs limits: it is repeated motion near the point of attention, which the vestibular-safe patterns guidance treats as a risk when it travels far or repeats many times.
Step-by-step resolution
Production code pattern
<div class="field">
<label for="email">Email</label>
<input id="email" name="email" type="email" aria-describedby="email-error" required>
<p class="field__error" id="email-error" data-state="empty"></p>
</div>
<p class="form__summary" role="status" aria-live="polite"></p>
.field__error {
opacity: 0;
translate: 0 -4px;
block-size: 0;
overflow: clip;
transition:
opacity var(--duration-200) linear,
translate var(--duration-200) var(--ease-decelerate),
block-size var(--duration-200) var(--ease-standard);
}
.field__error[data-state="shown"] {
opacity: 1;
translate: 0 0;
block-size: auto; /* with interpolate-size on :root */
}
.field input[aria-invalid="true"] {
border-color: var(--color-danger-600);
transition: border-color var(--duration-100) linear;
}
/* Shake: small, brief, and never the only signal. */
.field[data-shake] input {
animation: nudge 220ms var(--ease-standard);
}
@keyframes nudge {
25% { translate: -3px 0; }
60% { translate: 2px 0; }
}
@media (prefers-reduced-motion: reduce) {
.field__error { transition: none; }
.field[data-shake] input { animation: none; } /* colour and message remain */
}
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
function showError(field, message) {
const input = field.querySelector('input');
const error = field.querySelector('.field__error');
error.textContent = message; // text set BEFORE it becomes visible
error.dataset.state = 'shown';
input.setAttribute('aria-invalid', 'true');
if (!reduce.matches) {
field.dataset.shake = '';
field.addEventListener('animationend', () => delete field.dataset.shake, { once: true });
}
}
form.addEventListener('submit', (e) => {
const invalid = validate(form); // returns fields with messages
if (!invalid.length) return;
e.preventDefault();
invalid.forEach(({ field, message }) => showError(field, message));
summary.textContent = `${invalid.length} field${invalid.length > 1 ? 's need' : ' needs'} attention.`;
invalid[0].field.querySelector('input').focus(); // focus after messages exist
});
Rendering Impact: composite for the message’s fade and slide, layout for its height growth, paint for the border colour. The height animation is bounded to one small element; for long messages, fade without animating height.
Setting textContent before making the element visible matters for announcements: a live region or description that changes while already visible can be announced twice or partially. Building the content first and revealing it once is the reliable order.
Timing and persistence
Validation messages must not disappear on their own. A message that fades out after a few seconds is unreadable for anyone who reads slowly, uses magnification and has to scroll to it, or is interrupted — and it fails WCAG’s timing expectations for content the user needs. Keep messages until the underlying problem is fixed, and remove them when the field becomes valid rather than on a timer.
Validation timing also affects how noisy the animation is. Validating on every keystroke makes messages appear and disappear while typing, which is visually busy and, with a live region, genuinely disruptive. Validate on blur and on submit, and once a field has an error, re-validate as the user types so the message can disappear as soon as it is fixed.
Verification checklist
Constraints and trade-offs
- Animating message height is layout work; acceptable for one short message, not for a long list.
- Live regions announce changes, so overusing them during typing is disruptive.
- Shake animations should be brief and small; some users prefer none at all.
- Moving focus on submit is helpful for keyboard users but can be disorienting if unexpected; announce first.
- Colour changes alone never communicate an error.
Frequently asked questions
Will a screen reader announce an animated error message?
Not because it animated. Link it with aria-describedby so it is read with the field, and use a live region for messages that appear without focus moving.
Should validation errors be announced on every keystroke?
No. Validate on blur and submit, and re-validate while typing only to clear an existing error. Continuous announcements are disruptive.
Is a shake animation acceptable?
Small and brief, yes, as an enhancement — but never as the only signal, and it should be removed under reduced motion.
Can error messages disappear automatically?
They should not. Keep them until the problem is fixed; timed removal fails users who need longer to read.
Related
- Focus & State Motion Accessibility — the parent topic
- Animating ARIA Live Region Updates — announcing changes that animate
- Choosing Safe Animation Durations and Thresholds — limits for shakes and nudges