WCAG 2.2 Animation Compliance Criteria
Three WCAG success criteria govern almost every animated interface: SC 2.2.2 Pause, Stop, Hide (Level A), SC 2.3.1 Three Flashes or Below Threshold (Level A), and SC 2.3.3 Animation from Interactions (Level AAA). None were written with animation-timeline or document.startViewTransition() in mind, so applying them takes deliberate mapping: scroll-linked motion is user-controlled in a way the 2.2.2 authors never anticipated, while scroll-triggered motion falls squarely under 2.3.3. This guide maps each criterion onto modern CSS animation APIs with testable patterns and an audit workflow, building on the prefers-reduced-motion implementation techniques covered elsewhere in the Accessibility & Inclusive Motion Standards section.
Pages in this section
- Meeting WCAG 2.2.2 Pause, Stop, Hide with Scroll-Driven Animations
- WCAG 2.3.3 Animation from Interactions: A Compliance Checklist
The three success criteria at a glance
| Criterion | Level | Requirement | Applies to |
|---|---|---|---|
| SC 2.2.2 Pause, Stop, Hide | A | Moving, blinking, or scrolling content that starts automatically, lasts more than 5 seconds, and appears alongside other content must have a pause, stop, or hide mechanism | Auto-playing loops, marquees, auto-advancing carousels, ambient background motion |
| SC 2.3.1 Three Flashes or Below Threshold | A | Nothing flashes more than three times per second, unless the flash is below the general and red flash thresholds | Strobing keyframes, rapid opacity toggles, bright view-transition crossfades |
| SC 2.3.3 Animation from Interactions | AAA | Motion animation triggered by interaction can be disabled, unless the animation is essential to the functionality or the information being conveyed | Scroll-triggered reveals, parallax, view transitions, hover and click effects |
The level distinction matters for prioritization: 2.2.2 and 2.3.1 are Level A — the legal-compliance floor in most jurisdictions — while 2.3.3 is Level AAA. In practice a prefers-reduced-motion override delivers 2.3.3 conformance almost for free, so most teams treat all three as one workstream.
Minimal working example
A scroll-driven reveal that is compliant with all three criteria out of the box — scroll-linked (so 2.2.2’s auto-start clause never triggers), flash-free, and disabled under reduced motion for 2.3.3:
/* Baseline: content visible without any animation support */
.reveal {
opacity: 1;
transform: none;
}
@supports (animation-timeline: view()) {
.reveal {
opacity: 0;
transform: translateY(1.5rem);
animation: reveal-in linear both;
animation-timeline: view(); /* progress bound to scroll position */
animation-range: entry 0% entry 70%;
}
@keyframes reveal-in {
to { opacity: 1; transform: none; } /* single fade, no strobing (2.3.1) */
}
/* SC 2.3.3: scroll is an interaction — the motion must be disableable */
@media (prefers-reduced-motion: reduce) {
.reveal {
animation: none;
animation-timeline: auto;
opacity: 1;
transform: none;
}
}
}
The structure encodes the compliance logic: the animation never starts without user input, its keyframes contain exactly one luminance change, and the media query provides the 2.3.3 disable mechanism.
SC 2.2.2 Pause, Stop, Hide mapped to scroll-driven animations
SC 2.2.2 has three trigger conditions, all of which must be true before a pause mechanism is required: the motion starts automatically, lasts more than five seconds, and is presented in parallel with other content. A pure scroll-driven animation — one whose only clock is animation-timeline: scroll() or view() — fails the first condition by construction. It does not start automatically; it advances exactly as far as the user scrolls, and stopping the scroll stops the motion. The user’s scroll gesture is the pause, stop, and hide mechanism.
That defense collapses the moment you mix in a time base. The patterns that re-enter 2.2.2 scope:
- Scroll-triggered, time-based animations — a
@keyframesloop started on viewport entry runs on the document timeline afterwards. If it loops longer than five seconds, it needs a control. - Auto-advancing carousels and marquee-style tickers — these run with or without scroll and are the canonical 2.2.2 failure.
- Ambient background motion — floating shapes, drifting gradients, video backgrounds.
The standard control pattern toggles animation-play-state from a real button (hover-pause alone is not a mechanism keyboard and touch users can operate):
.ticker { animation: ticker-scroll 20s linear infinite; }
.ticker.is-paused { animation-play-state: paused; }
const btn = document.querySelector('.ticker-pause');
btn.addEventListener('click', () => {
const paused = document.querySelector('.ticker').classList.toggle('is-paused');
btn.setAttribute('aria-pressed', String(paused));
btn.textContent = paused ? 'Play ticker' : 'Pause ticker';
});
The full analysis — including when scroll-linked motion still needs a stop mechanism — is in Meeting WCAG 2.2.2 Pause, Stop, Hide with Scroll-Driven Animations.
SC 2.3.1 Three Flashes mapped to modern animation APIs
SC 2.3.1 is absolute: no content may flash more than three times in any one-second period unless the flashing area is small and dim enough to fall below the defined thresholds. The modern-API surfaces to audit:
- Strobing keyframes — any
@keyframesthat alternates between high and low luminance (or to and from saturated red) more than three times per second.animation: blink 0.1s infinite alternateis a failure regardless of what triggers it. - Scroll-scrubbed flicker — a scroll-driven animation whose keyframes cycle opacity several times across its
animation-rangecan be scrubbed fast enough by a quick scroll or a flicked trackpad to exceed three luminance transitions per second. Keyframes under scroll timelines should change luminance monotonically: fade in once, not in-out-in. - View-transition flashes — the default crossfade is safe, but a custom
::view-transition-new(root)animation that flashes white, combined with rapid repeated navigations (back/forward spamming), can strobe the full viewport. Full-viewport flashes have no size exemption.
/* FAILS 2.3.1 when scrubbed quickly: three luminance cycles in one range */
@keyframes flicker-in {
0%, 40%, 80% { opacity: 0; }
20%, 60%, 100% { opacity: 1; }
}
/* PASSES: one monotonic luminance change across the scroll range */
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
Because 2.3.1 is about physiological harm rather than preference, prefers-reduced-motion is not a mitigation for it — the criterion must hold for every user.
SC 2.3.3 Animation from Interactions mapped to scroll and navigation
SC 2.3.3 requires that “motion animation triggered by interaction can be disabled, unless the animation is essential.” The key insight for this site’s subject matter: scrolling is an interaction. Every scroll-driven reveal, every parallax layer, every scroll-linked scale or rotation is interaction-triggered animation under this criterion. So is every view transition, because navigation is an interaction too.
The normatively sanctioned disable mechanism is the OS-level motion preference exposed through prefers-reduced-motion. The prefers-reduced-motion CSS override patterns — resetting animation-timeline: auto alongside animation: none, and guarding startViewTransition() in JavaScript — are exactly the techniques 2.3.3 conformance requires. Two refinements worth adopting beyond the binary override:
- Replace, don’t just remove. 2.3.3 targets motion animation — transform-based movement, scaling, spinning, parallax depth. A cross-fade conveys the same state change without spatial displacement, so swapping a slide-in for a fade under reduced motion preserves the design intent while conforming. The vestibular-safe motion design patterns catalog which motion types must go and which opacity- or color-based substitutes work.
- Scale where full removal harms usability. For interfaces where motion carries orientation cues, motion scaling through user preferences reduces amplitude and duration proportionally instead of hard-disabling — still a valid “disable the motion” outcome when the residual movement is non-vestibular.
The item-by-item audit lives in the WCAG 2.3.3 compliance checklist.
Criteria-to-technique reference table
| Animation pattern | 2.2.2 (A) | 2.3.1 (A) | 2.3.3 (AAA) | Primary technique |
|---|---|---|---|---|
Scroll-scrubbed reveal (view()) |
Passes — scroll is the control | Keep keyframes monotonic | Override required | animation-timeline: auto under reduced motion |
Scroll progress bar (scroll()) |
Passes — scroll is the control | No luminance cycling | Usually essential (conveys position) — document the exemption | Keep; optionally simplify |
| Parallax layers | Passes if purely scroll-linked | Low risk | Override required | Flatten transform deltas to zero under reduced motion |
| Scroll-triggered time-based loop | Control needed if > 5 s | Audit loop keyframes | Override required | Pause button + reduced-motion override |
| Auto-advancing carousel / ticker | Pause control required | Audit slide flashes | Out of scope unless interaction-started | animation-play-state toggle with aria-pressed |
| Same-document view transition | Passes if < 5 s one-shot | No white-flash keyframes | Skip or reduce | matchMedia guard around startViewTransition() |
Cross-document view transition (@view-transition) |
Passes if < 5 s one-shot | No white-flash keyframes | Skip or reduce | Reduced-motion media query on the @view-transition opt-in styles |
| Hover / focus micro-interactions | Not in scope (short) | Low risk | Override transform-based motion | transition-duration token reset |
Audit workflow
A repeatable six-step pass you can run per release:
- Inventory. Grep the codebase for
animation,transition,animation-timeline,view-transition-name,startViewTransition, and.animate(. Every hit is an audit row. - Classify. For each row record: trigger (auto / scroll-linked / interaction), clock (scroll timeline / document timeline), worst-case duration, and luminance behavior of its keyframes.
- Test 2.2.2. Filter for auto-start + over five seconds + parallel with content. Each survivor needs a keyboard-operable pause, stop, or hide control. Verify with a keyboard only — no mouse.
- Test 2.3.1. Scrub every scroll-driven animation at maximum flick speed and spam navigation on view transitions while recording. Count luminance transitions per second in the recording; anything above three fails.
- Test 2.3.3. In DevTools, emulate
prefers-reduced-motion: reduce(Rendering tab), then rundocument.getAnimations().filter(a => a.playState !== 'idle')while scrolling and navigating — the array should contain only animations you have documented as essential. - Lock it in. Encode steps 3–5 as Playwright tests using
page.emulateMedia({ reducedMotion: 'reduce' })so regressions surface in CI rather than in an accessibility complaint.
Browser support and @supports guard
The criteria apply regardless of engine, but the APIs you are auditing do not ship uniformly. animation-timeline is supported in Chrome 115 and Edge 115; Firefox implements it behind a pref (available in Nightly); Safari support is in active development in Technology Preview builds. Same-document view transitions ship in Chrome 111 and Safari 18, with Firefox support in development; cross-document @view-transition requires Chrome 126+ or Safari 18.2+.
This asymmetry has a compliance consequence: an animation that does not run cannot fail an audit, so browsers without animation-timeline support get the static baseline and are trivially conformant. Structure your CSS so the enhancement and its accessibility override travel together:
@supports (animation-timeline: scroll()) {
.parallax-layer {
animation: depth-shift linear both;
animation-timeline: scroll(root);
}
/* The 2.3.3 override lives inside the same guard —
it exists exactly where the motion exists */
@media (prefers-reduced-motion: reduce) {
.parallax-layer {
animation: none;
animation-timeline: auto;
}
}
}
The progressive enhancement patterns for animation features cover the guard architecture in depth; the compliance-specific rule is simply that no @supports block introducing motion may ship without its reduced-motion counterpart in the same block.
Gotchas and failure modes
-
Treating scroll-linked as a universal 2.2.2 pass. The exemption holds only while scroll position is the sole clock. A “scroll-triggered” animation that starts on viewport entry but then runs on time is auto-playing content the moment the user stops scrolling — the most commonly missed reclassification in audits.
-
Using
prefers-reduced-motionas a 2.3.1 fix. Flash-threshold violations harm users who have never configured a motion preference. Strobing keyframes must be fixed in the default experience, not gated behind a media query. -
Hover-only pause on carousels.
animation-play-state: pausedon:hoveris invisible to keyboard and touch users. 2.2.2 requires a mechanism — an operable, focusable control with an accessible name and state (aria-pressed). -
Forgetting
animation-timelinein the 2.3.3 override.animation: nonedoes not resetanimation-timeline, so a scroll timeline can remain attached and re-engage the moment any animation is reapplied. Always pair the two resets. -
Claiming “essential” too broadly. The exemption covers animation whose removal would break functionality or information — a scroll progress indicator, a drag ghost. A parallax hero is never essential. Document each exemption claim; auditors ask.
-
View-transition duration overrides without a JS guard. Near-zero
::view-transition-*durations hide the motion but still allocate snapshot layers; skippingstartViewTransition()under reduced motion is cleaner and faster.
Compliance checklist
- Every auto-playing animation longer than five seconds has a keyboard-operable pause, stop, or hide control with visible state.
- No keyframe set produces more than three luminance transitions per second — including when scrubbed by fast scrolling.
- Every scroll-driven animation resets both
animation: noneandanimation-timeline: autounderprefers-reduced-motion: reduce. - Every
startViewTransition()call site checksmatchMedia('(prefers-reduced-motion: reduce)')first. - Transform-based motion has an opacity- or color-based replacement under reduced motion where the state change must remain visible.
- Essential-motion exemptions are documented with a rationale per element.
- Playwright CI runs assert zero non-essential active animations under
reducedMotion: 'reduce'. - The audit inventory is re-run whenever a new
animation-timelineorview-transition-namelands in review.
Related
- Meeting WCAG 2.2.2 Pause, Stop, Hide with Scroll-Driven Animations — the five-second rule, marquee patterns, and pause-control implementation in depth
- WCAG 2.3.3 Animation from Interactions: A Compliance Checklist — item-by-item AAA audit for scroll-triggered and navigation-triggered motion
- Motion Preference Support Matrix: Browsers & Operating Systems — where
prefers-reduced-motionand its OS toggles are available, per engine and platform - Building Reduced-Motion Variants of View Transitions — crossfade substitutes that keep navigation feedback while conforming to 2.3.3
- Understanding the CSS Scroll Timeline API — the
scroll()andview()semantics that make scroll-linked motion user-controlled