Accessible Animation: A WCAG 2.2 Checklist for React
By Hizmedia · · 6 min read
Animation touches more of WCAG than most people expect: not only motion itself, but contrast over moving backgrounds, focus on custom controls, screen readers meeting split-up text, and status changes that only show visually. This checklist covers the criteria that come up again and again when auditing animated React components, with what to do for each.
Motion itself
- 2.2.2 Pause, Stop, Hide (A): anything moving on its own for more than five seconds, next to other content, can be paused, stopped or hidden. Marquees, carousels and ambient loops need a control, or must stop on their own within five seconds.
- 2.3.1 Three Flashes or Below Threshold (A): nothing flashes more than three times in any one second. Watch fast flickers, strobing glitch effects and quick colour swaps.
- 2.3.3 Animation from Interactions (AAA): motion triggered by interaction, like parallax on scroll or elements following the cursor, can be turned off. Honouring prefers-reduced-motion is the usual way to meet it.
Text and contrast
- 1.4.3 Contrast (Minimum) (AA): text needs 4.5:1 against what's behind it (3:1 for large text), and that includes the brightest moment of an animated background. Check the worst frame, not the average.
- 1.4.11 Non-text Contrast (AA): the parts of controls people need to see, like a toggle's knob or a slider's thumb, need 3:1 against their surroundings.
- Split text needs care: effects that wrap every letter or word in its own element can be read letter by letter by screen readers. Hide the animated pieces with
aria-hiddenand give the whole sentence once in a visually hidden element.
<h1>
<span className="sr-only">Ship pages that feel alive</span>
<span aria-hidden>
{words.map((word) => (
<motion.span key={word} className="inline-block">{word} </motion.span>
))}
</span>
</h1>Controls and keyboards
- 2.1.1 Keyboard (A): every animated control works from the keyboard. A hold-to-confirm button should accept holding Space or Enter; a drag-to-dismiss toast needs a dismiss button too.
- 2.4.7 Focus Visible (AA): focus rings stay visible on animated elements. Don't let a transform or an overflow clip hide them.
- 4.1.2 Name, Role, Value (A): custom controls expose what they are. A styled switch needs
role="switch"andaria-checked, a like buttonaria-pressed, tabs the full tablist pattern. Building on nativebuttonandinputelements gets most of this for free. - 2.5.7 Dragging Movements (AA): anything you can drag, like a slider, a card stack or a compare handle, also has a way to do it without dragging, such as clicks or arrow keys.
Changes people need to hear about
4.1.3 Status Messages (AA): when an animation shows a result, like a submit button turning into a tick or a toast sliding in, screen readers need the same message. Put it in a live region, and keep focus where it was.
<button aria-busy={saving} onClick={save}>{saving ? <Spinner /> : "Save"}</button>
{/* announced without moving focus */}
<span role="status" className="sr-only">{saved ? "Saved" : ""}</span>Make it a habit
Audit each component once, write down what you found, and keep the notes with the component. Every MotionKit component page carries a report like that, covering keyboard, screen readers, reduced motion and contrast, so you can see what was checked and what to watch for when you change colours or copy.