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
How the three criteria partition your animation surface The animation surface is split by trigger. Motion that starts by itself and runs beyond five seconds belongs to SC 2.2.2. Motion the user triggers belongs to SC 2.3.3. Flashing belongs to SC 2.3.1 regardless of trigger, which is why it is drawn as a band crossing both halves. the partition is by trigger, not by technique Starts by itself SC 2.2.2 Pause, Stop, Hide β€” Level A marquees, tickers, auto-advancing carousels, ambient background drift β€” only past five seconds Started by the user SC 2.3.3 Animation from Interactions β€” AAA scroll reveals, parallax, hover and click effects, view transitions β€” any duration SC 2.3.1 Three Flashes β€” Level A β€” crosses both halves nothing may flash more than three times per second, whoever started it A pure scroll-linked animation stops when scrolling stops, so it never enters the 2.2.2 half at all.

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.
Three common mis-classifications Three failure patterns: treating a scroll-triggered but time-clocked loop as exempt because scrolling started it, assuming a prefers-reduced-motion override satisfies 2.2.2 when the user has not set the preference, and auditing each animation alone so a page-wide flash rate is never measured. "The user started it, so 2.2.2 does not apply" An entry-triggered loop is clocked by time, not by scroll. Past five seconds it needs a control. "prefers-reduced-motion covers us" It covers users who set the preference. 2.2.2 asks for a control every visitor can reach. "Each animation was checked individually" 2.3.1 is measured over the rendered viewport β€” several slow fades can still combine into a flash.
  1. 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.

  2. 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).

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

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

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

Frequently Asked Questions

Does a scroll-driven animation ever need a pause control?

Not when it is genuinely scroll-linked. SC 2.2.2 applies to content that starts automatically, and a pure scroll-driven animation does not start until the user scrolls and stops the moment they stop. The user is the clock, and a user who wants it to stop simply stops scrolling.

The distinction that matters is between scroll-linked and scroll-triggered. An animation whose progress is the scroll position is linked. An animation that a scroll position starts and that then runs on its own clock β€” a looping background, an auto-advancing carousel revealed on entry β€” is triggered, and it is subject to the criterion like any other automatic motion.

Is 2.3.3 worth pursuing when it is only Level AAA?

For a motion-forward site, yes, and the cost is usually near zero. If the site already implements a reduced-motion override properly, 2.3.3 conformance is largely a matter of confirming coverage rather than building anything new. The criterion asks that interaction-triggered motion animation can be disabled unless it is essential, which is exactly what the override does.

The level is a statement about how universally a criterion applies, not about how much it matters to your users. On a site whose navigation and reading experience are built from motion, the AAA criterion describes a larger share of the experience than several of the A criteria do.

How is the three-flashes threshold measured on an animated page?

Over the rendered viewport, not per element. That is the part teams get wrong: three separate elements each pulsing at two cycles per second can combine into a viewport-level flash rate above the threshold even though no single element exceeds it. The measurement is of the relative luminance change across the visible area, which is why a page-wide review β€” scrolled at speed, with all effects running β€” is the only reliable check.

In practice, few scroll-driven designs approach the threshold; the risk concentrates in loading states, rapid crossfades between high-contrast images, and error states that flash for emphasis.

Do view transitions count as motion animation from interactions?

Yes, when they displace anything. A transition that morphs a card into a full-width banner is spatial displacement triggered by a click, which is squarely in scope. A transition that only crossfades between two states is animation without motion, and is out of scope for 2.3.3 in the same way an opacity fade is.

This is why the reduced-motion variant of a transition is usually a crossfade rather than a skip: it removes the displacement that brings the transition into scope while keeping the change signal that makes the navigation comprehensible.


Up: Accessibility & Inclusive Motion Standards