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 useonpaste="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 thatforced-colors: activemapsButtonText/ButtonFacecorrectly. - ALWAYS test under Windows High Contrast Mode (Settings > Accessibility > Contrast themes) before shipping anything with custom colors. NEVER rely on
box-shadowfor visual definition ; HCM forces it tonone.
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 byprefers-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
- W3C : WCAG 2.2 Recommendation (verified 2026-05-19)
- W3C : Understanding SC 2 5 8 Target Size (Minimum) (verified 2026-05-19)
- W3C : Understanding SC 2 4 11 Focus Not Obscured (Minimum) (verified 2026-05-19)
- W3C : Understanding SC 2 5 7 Dragging Movements (verified 2026-05-19)
- W3C : Understanding SC 3 3 8 Accessible Authentication (Minimum) (verified 2026-05-19)
- W3C : WCAG 3 0 Working Draft (verified 2026-05-19)
- MDN : prefers-reduced-motion (verified 2026-05-19)
- MDN : prefers-contrast (verified 2026-05-19)
- MDN : forced-colors (verified 2026-05-19)
- MDN : prefers-reduced-data (verified 2026-05-19)
- MDN : prefers-reduced-transparency (verified 2026-05-19)
- SAPC-APCA repository (verified 2026-05-19)