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


WCAG success criteria versus animation type matrix A three-by-three grid. Rows are success criteria 2.2.2, 2.3.1, and 2.3.3; columns are scroll-driven animations, view transitions, and auto-playing motion. Each cell names the required mitigation, with shading marking where active work is usually needed: pause controls for auto-playing motion under 2.2.2, and prefers-reduced-motion overrides for scroll-driven and view-transition motion under 2.3.3. Scroll-driven scroll() / view() View transitions startViewTransition() Auto-playing time-based loops SC 2.2.2 Pause, Stop, Hide Level A Scrolling is the pause mechanism — usually passes One-shot and under 5 seconds — usually passes Pause control required if over 5 s beside other content SC 2.3.1 Three Flashes Level A Audit strobing keyframes and rapid opacity toggles Avoid full-viewport bright flashes on rapid navigation Max 3 flashes per second, at any size, no exceptions SC 2.3.3 Animation from Interactions, AAA Scroll is interaction: disable via prefers- reduced-motion Skip or provide a reduced-motion variant Out of scope unless an interaction starts the loop shaded = active mitigation work required in typical builds

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 @keyframes loop 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 @keyframes that alternates between high and low luminance (or to and from saturated red) more than three times per second. animation: blink 0.1s infinite alternate is a failure regardless of what triggers it.
  • Scroll-scrubbed flicker — a scroll-driven animation whose keyframes cycle opacity several times across its animation-range can 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:

  1. 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.
  2. 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:

  1. Inventory. Grep the codebase for animation, transition, animation-timeline, view-transition-name, startViewTransition, and .animate(. Every hit is an audit row.
  2. Classify. For each row record: trigger (auto / scroll-linked / interaction), clock (scroll timeline / document timeline), worst-case duration, and luminance behavior of its keyframes.
  3. 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.
  4. 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.
  5. Test 2.3.3. In DevTools, emulate prefers-reduced-motion: reduce (Rendering tab), then run document.getAnimations().filter(a => a.playState !== 'idle') while scrolling and navigating — the array should contain only animations you have documented as essential.
  6. 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

  1. 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.

  2. Using prefers-reduced-motion as 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.

  3. Hover-only pause on carousels. animation-play-state: paused on :hover is invisible to keyboard and touch users. 2.2.2 requires a mechanism — an operable, focusable control with an accessible name and state (aria-pressed).

  4. Forgetting animation-timeline in the 2.3.3 override. animation: none does not reset animation-timeline, so a scroll timeline can remain attached and re-engage the moment any animation is reapplied. Always pair the two resets.

  5. 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.

  6. View-transition duration overrides without a JS guard. Near-zero ::view-transition-* durations hide the motion but still allocate snapshot layers; skipping startViewTransition() 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: none and animation-timeline: auto under prefers-reduced-motion: reduce.
  • Every startViewTransition() call site checks matchMedia('(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-timeline or view-transition-name lands in review.

Up: Accessibility & Inclusive Motion Standards