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.

One validation failure, three channelsThe animation is one channel of three; the other two must work without it.One validation failure, three channelsValidationfailsaria-invalidsetprogrammaticMessage linkedaria-describedbySummaryannouncedlive regionMessageanimates invisual
The animation is one channel of three; the other two must work without it.

Step-by-step resolution

Accessible animated validationWire the semantics first, then animate the appearance.Accessible animated validation1Render an empty message element per field, linked with aria-describedby.The relationship exists before any error2On failure, set the message text and aria-invalid together.Complete content when it appears3Animate the message's opacity and a small slide.Visible arrival4On submit, put a summary into a polite live region.Announced without moving focus5Move focus to the first invalid field after the summary.Users land where they can fix it6Remove shakes and slides under reduced motion.
Wire the semantics first, then animate the appearance.

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.

What each channel must deliverIf the animation were removed entirely, the first two rows would still work.What each channel must deliverMechanismMust work without motion?Field marked invalidaria-invalid on the inputYesMessage reachablefrom the fieldaria-describedbyYesSubmit summaryannouncedPolite live regionYesAttention drawn tothe errorFade, slide, optional shakeNo: enhancementColour signallingBorder and text colourNever colour alone
If the animation were removed entirely, the first two rows would still work.

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.