SuperAPI AI Directory

frontend-a11y-motion-contrast-wcag22

Frontend & websites

View repository

Updated 10-15 22:18

About this skill

Frontend A11y : Motion, Contrast, WCAG 2.2

This skill is the operational reference for WCAG 2.2 conformance, contrast measurement, and user-preference media features (prefers-reduced-motion, prefers-contrast, forced-colors, prefers-reduced-data, prefers-reduced-transparency). It covers the nine WCAG 2.2 Success Criteria added over 2.1, the binding 2.x contrast ratios, motion-reduction patterns, palette swaps for elevated contrast preferences, and Windows High Contrast Mode support via system color keywords. The skill does NOT cover focus / keyboard mechanics (see [[frontend-a11y-focus-keyboard-inert]]), ARIA roles (see [[frontend-a11y-aria-patterns]]), color-palette generation (see [[frontend-theming-color-palette-oklch]]), or INP performance measurement (see [[frontend-perf-core-web-vitals-inp]]).

Quick Reference

Floor rules

  • ALWAYS check @media (prefers-reduced-motion: reduce) BEFORE shipping any non-essential motion. NEVER ship parallax, auto-play carousels, slide-in transitions, or scale-on-hover without a reduce-branch fallback.
  • ALWAYS size interactive targets to a 24-by-24 CSS-pixel solid square (WCAG 2.2 SC 2 5 8). NEVER ship a 16-by-16 icon button unless one of the five exceptions applies (spacing, equivalent, inline, user-agent, essential).
  • ALWAYS provide a single-pointer (click or tap) alternative when an interaction uses dragging (WCAG 2.2 SC 2 5 7). NEVER ship a drag-only slider, kanban, or color wheel.
  • ALWAYS allow paste in password fields and support autocomplete="current-password" (WCAG 2.2 SC 3 3 8). NEVER use onpaste="return false" or block password-manager autofill.
  • ALWAYS measure contrast on the RENDERED foreground-background pair. NEVER trust the declared token value when the surface uses opacity, gradient, or backdrop-filter.
  • ALWAYS treat APCA as informational and cite WCAG 2.x ratios for conformance. NEVER tell a stakeholder that an APCA Lc 60 / 75 / 90 result satisfies legal WCAG 2.2 conformance.
  • ALWAYS prefer native HTML elements over <div role="button"> so that forced-colors: active maps ButtonText / ButtonFace correctly.
  • ALWAYS test under Windows High Contrast Mode (Settings > Accessibility > Contrast themes) before shipping anything with custom colors. NEVER rely on box-shadow for visual definition ; HCM forces it to none.

Decision tree 1 : Which contrast SC applies ?

Is the foreground TEXT (regular glyphs that convey meaning) ?
  -> Is the text "large" : 18 pt and above, OR 14 pt bold and above ?
       -> Yes : SC 1 4 3 AA requires 3 to 1. SC 1 4 6 AAA requires 4 5 to 1.
       -> No : SC 1 4 3 AA requires 4 5 to 1. SC 1 4 6 AAA requires 7 to 1.

Is the foreground a UI COMPONENT BOUNDARY or a GRAPHICAL OBJECT
(border of input, focus ring, custom checkbox indicator, icon-only button) ?
  -> SC 1 4 11 Non-text Contrast AA requires 3 to 1 against adjacent colors.

Does the icon-only button contain a GLYPH that reads as text
(letter, digit, punctuation) ?
  -> Treat as text. SC 1 4 3 applies. 4 5 to 1 or 3 to 1 by size.

Pure decorative graphics (logos when not used to identify a control,
purely cosmetic illustrations) ?
  -> SC 1 4 3 and 1 4 11 do NOT apply. No ratio required.

Decision tree 2 : Motion strategy ?

Is the motion FUNCTIONAL : it conveys a state change (open / close, error
appearance, save confirmation) that the user MUST perceive ?
  -> Inside `@media (prefers-reduced-motion: reduce)` REPLACE the keyframes
     with an `opacity` or `color` crossfade. Do NOT remove the transition entirely ;
     the user still needs the cue.

Is the motion DECORATIVE : parallax scrolling, hero-section drift, hover
scale-up, auto-play carousel ?
  -> Place the entire animation INSIDE `@media (prefers-reduced-motion: no-preference)`.
     Users who set "reduce" never see it.

Are you calling `document.startViewTransition()` ?
  -> Skip the call when `matchMedia('(prefers-reduced-motion: reduce)').matches`,
     or override `::view-transition-group(*) { animation: none; }` inside the reduce block.

Are you using GSAP / Framer Motion / Motion One ?
  -> Read `window.matchMedia('(prefers-reduced-motion: reduce)').matches` and
     attach an `addEventListener('change', ...)` to react to live OS-setting changes.

Decision tree 3 : Does this target meet 2 5 8 ?

Bounding box at least 24 by 24 CSS pixels ?
  -> PASS.

Smaller, but a 24-CSS-pixel-diameter circle centered on the target does
NOT intersect any other target circle ?
  -> PASS (spacing exception).

Target is INLINE in running text, sized by surrounding line-height
(e.g. a link inside body copy) ?
  -> PASS (inline exception).

A different control on the same page provides the EQUIVALENT function
and meets 24 by 24 ?
  -> PASS (equivalent-control exception).

Size is set by the USER AGENT and unmodified by author
(native `<input type="date">` calendar picker) ?
  -> PASS (UA exception).

Particular presentation is ESSENTIAL or LEGALLY REQUIRED
(map pin at GPS coordinates, paper-form replica required by law) ?
  -> PASS (essential exception).

None of the above ?
  -> FAIL. Increase the hit area to 24 by 24 via `padding` or a larger
     wrapper, OR relayout to satisfy the spacing exception.

Decision tree 4 : Forced colors versus prefers-contrast ?

Is the user agent currently overriding author colors with a system palette
(Windows High Contrast Mode, Edge / Chrome on Windows with HCM enabled) ?
  -> `@media (forced-colors: active)` matches. Use system color keywords :
     Canvas, CanvasText, LinkText, ButtonText, ButtonFace, Highlight, etc.
     Add an explicit `border: 1px solid ButtonText` because `box-shadow` is
     forced to `none`.

Did the user request MORE contrast at the OS level
(macOS Increase Contrast, Windows "make text bigger" with high-contrast theme,
iOS Increase Contrast) ?
  -> `@media (prefers-contrast: more)` matches. Swap palette tokens to
     higher-contrast values. The page is still using AUTHOR colors.

Did the user request LESS contrast (photosensitivity, migraine sensitivity) ?
  -> `@media (prefers-contrast: less)` matches. Reduce border weight, soften
     accents, ensure no harsh pure-black-on-pure-white.

Did the user set a custom palette that does not map to "more" or "less" ?
  -> `@media (prefers-contrast: custom)` matches. Same remediation as `more`
     in practice : provide a defensive high-contrast token set.

Patterns

Pattern : opt-in animation gated by no-preference

.card {
  transform: translateY(0);
  opacity: 1;
}

@media (prefers-reduced-motion: no-preference) {
  .card {
    animation: slide-in 600ms ease-out;
  }
}

@keyframes slide-in {
  from { transform: translateY(24px); opacity: 0; }
  to   { transform: translateY(0);    opacity: 1; }
}

Users who never expressed a preference get the slide. Users who set "reduce" never see motion ; the entire keyframe rule is absent.

Pattern : reduce branch replaces motion with crossfade

@keyframes slide-up   { from { transform: translateY(24px); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes cross-fade { from { opacity: 0; } to { opacity: 1; } }

.toast { animation: slide-up 300ms ease-out; }

@media (prefers-reduced-motion: reduce) {
  .toast { animation: cross-fade 150ms linear; }
}

Functional state-change cue preserved ; vestibular trigger (translateY) removed.

Pattern : View Transitions respect prefers-reduced-motion

function update(stateChange) {
  const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  if (reduce || !document.startViewTransition) {
    stateChange();
    return;
  }
  document.startViewTransition(stateChange);
}

Or, alternatively, leave the call and disable the cross-fade in CSS :

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Pattern : prefers-contrast palette swap

:root {
  --text:   oklch(0.30 0.02 250);
  --bg:     oklch(0.98 0.00 0);
  --border: oklch(0.85 0.01 250);
}

@media (prefers-contrast: more) {
  :root {
    --text:   black;
    --bg:     white;
    --border: black;
  }
}

@media (prefers-contrast: less) {
  :root {
    --text:   oklch(0.40 0.02 250);
    --border: oklch(0.90 0.01 250);
  }
}

Always verify the swapped tokens still meet 4 5 to 1 (or 7 to 1 for AAA targets) against their surfaces.

Pattern : forced-colors with system color keywords

.btn {
  background: var(--accent);
  color: white;
  border: 1px solid transparent;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}

@media (forced-colors: active) {
  .btn {
    border-color: ButtonText;
    forced-color-adjust: auto;
  }
}

Under forced-colors: active, box-shadow is forced to none and the colored background may be overridden by the system palette. The explicit border: 1px solid ButtonText keeps the button visually defined.

Pattern : target size with padding and the spacing exception

.icon-btn {
  width: 24px;
  height: 24px;
  padding: 0;
}

.toolbar .icon-btn + .icon-btn {
  margin-left: 24px;
}

Either size up to a 24-by-24 hit area, or keep the icon smaller and ensure 24-pixel center-to-center spacing so the spacing exception applies.

Pattern : focus-not-obscured remediation

:root {
  --header-height: 64px;
}

html {
  scroll-padding-top: var(--header-height);
}

:focus-visible {
  scroll-margin-top: var(--header-height);
}

Programmatic scroll-on-focus offsets the sticky header by exactly the header height. Combine with :focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } to also meet 2 4 13 Focus Appearance.

Pattern : accessible authentication

<form>
  <label for="pw">Password</label>
  <input
    id="pw"
    type="password"
    autocomplete="current-password"
    name="password"
  />
  <button type="submit">Sign in</button>
</form>

NO onpaste="return false", NO blocked autofill. For high-assurance flows, offer passkey (WebAuthn) as the primary path so the cognitive-function test never applies.

Pattern : prefers-reduced-transparency opt-out

.nav {
  background: oklch(0.99 0 0 / 0.7);
  backdrop-filter: blur(12px);
}

@media (prefers-reduced-transparency: reduce) {
  .nav {
    background: oklch(0.99 0 0);
    backdrop-filter: none;
  }
}

Limited Availability as of 2026-05-19 ; ship the rule anyway for forward compatibility.

Conformance ratios at a glance

SC Level Foreground Ratio
1 4 3 Contrast (Minimum) AA Normal text 4 5 to 1
1 4 3 Contrast (Minimum) AA Large text (18 pt or 14 pt bold) 3 to 1
1 4 6 Contrast (Enhanced) AAA Normal text 7 to 1
1 4 6 Contrast (Enhanced) AAA Large text 4 5 to 1
1 4 11 Non-text Contrast AA UI components, graphical objects 3 to 1
2 4 13 Focus Appearance AAA Focused versus unfocused state 3 to 1

Verified against WCAG 2.2 Recommendation (verified 2026-05-19).

APCA + WCAG 3 note

APCA (Accessible Perceptual Contrast Algorithm) produces an Lc score from -108 to +106 with thresholds Lc 60 (basic body text), Lc 75 (enhanced), Lc 90 (high-impact). APCA accounts for polarity, font weight, and spatial frequency where WCAG 2.x luminance ratios do not.

Normative status as of 2026-05-19 : NOT normative. WCAG 3 is a Working Draft (verified 2026-05-19) with the explicit stability disclaimer "It is inappropriate to cite this document as other than a work in progress." The Working Draft does NOT include APCA as a binding metric. The binding conformance metric is the WCAG 2 x luminance ratio.

Practical recommendation : design tokens that pass both WCAG 2 2 (4 5 to 1 for normal text) AND APCA Lc 60 are future-safe. Until WCAG 3 reaches Candidate Recommendation, cite the 2 2 ratio for audits and contracts.

Cross-references

  • [[frontend-a11y-aria-patterns]] : ARIA roles, states, properties, custom-control name / role / value.
  • [[frontend-a11y-focus-keyboard-inert]] : focus management, :focus-visible, inert, keyboard interaction patterns.
  • [[frontend-theming-color-palette-oklch]] : OKLCH palette generation, shade ladders, contrast-aware token math.
  • [[frontend-impl-view-transitions-scroll-animations]] : View Transitions API and scroll-driven animations both gated by prefers-reduced-motion.

Reference Links

  • references/methods.md : all nine new WCAG 2 2 SCs with normative text, media-feature value lists, full system color keyword surface.
  • references/examples.md : renderable HTML fragment demonstrating motion-respecting, contrast-meeting, target-size-compliant patterns.
  • references/anti-patterns.md : ten anti-patterns with WCAG-SC mapping, symptom, root cause, and fix.

Authoritative sources