accessibility variants

[check: …]
📖 Pair this live Tailwind demo with the companion guide (.md) — this page is the rendered ground truth. ↗ Builds on frontend/tailwind: responsive variants — responsive variants react to the viewport; a11y variants react to the user (their OS motion / contrast / input preferences).

every a11y variant = one @media query (or pseudo-class)

Tailwind does not invent these — it wraps the real CSS Media Queries Level 5 and pseudo-classes the browser already honors from the OS. motion-safe: / motion-reduce: read prefers-reduced-motion; contrast-more: reads prefers-contrast; forced-colors: reads forced-colors (Windows High Contrast). focus-visible: is the :focus-visible pseudo — the keyboard-focus-only ring. sr-only is a utility, not a variant: it clips text to a 1×1 box so it is invisible on screen but still announced by screen readers.

variant / utilitycompiles tofires when…
motion-safe: @media (prefers-reduced-motion: no-preference) user has NOT turned on "Reduce Motion" — safe to animate
motion-reduce: @media (prefers-reduced-motion: reduce) user HAS turned on "Reduce Motion" — give them a still fallback
contrast-more: @media (prefers-contrast: more) user enabled "Increase Contrast" — add stronger borders / outlines
forced-colors: @media (forced-colors: active) Windows High Contrast mode — palette is overridden to a 16-color set
focus-visible: :focus-visible pseudo-class element has focus AND the browser heuristic says "show a ring" (keyboard, not mouse)
focus-within: :focus-within pseudo-class element OR any descendant has focus (great for wrapping labels)
sr-only (utility) position:absolute; width:1px; height:1px; overflow:hidden; … always — visually hidden, still in the a11y tree
not-sr-only (utility) undoes the above (resets position / size / overflow) show-on-focus pattern: reveal the skip-link when it receives focus

1 · motion-safe / motion-reduce — respect the user's motion choice

The block below carries motion-safe:animate-bounce: the bounce keyframes only exist when prefers-reduced-motion is not set to reduce. If a user has motion sickness or vestibular disorder and enabled Reduce Motion, the animation is silently absent — no fallback code, no JS feature-detect. You can pair it with motion-reduce: for an explicit still state.

🏀 bouncing only if motion is OK

(if this is static, your OS has Reduce Motion ON — the variant honored it)

prefers-reduced-motion: …

.motion-safe\:animate-bounce { @media (prefers-reduced-motion: no-preference) { animation: bounce 1s infinite; /* ← keyframes only exist here */ } } .motion-reduce\:underline { /* explicit fallback */ @media (prefers-reduced-motion: reduce) { text-decoration: underline; } }
To see it flip: macOS System Settings → Accessibility → Display → Reduce motion, Windows Settings → Accessibility → Visual effects → Animation effects.

2 · focus-visible — the a11y-correct focus ring

focus: fires on any focus (mouse click too) — ugly ring on every click. focus-visible: fires only when the browser's heuristic decides the user is keyboard-navigating (Tab key). The canonical accessible pattern is focus:outline-none focus-visible:ring-2: kill the mouse-click ring, keep the keyboard ring. Try it: click each button (no ring), then press Tab to move focus between them (cyan ring appears).

A & B: ring only on keyboard. C: ring on every focus (the old way).

:focus-visible match: … · box-shadow: …

"simulate" calls .focus({'{ focusVisible: true }'}) — a Chromium-only option that forces keyboard-style focus. Firefox / Safari need a real Tab press (heuristic-gated).

3 · sr-only — visually hidden, screen-reader visible

Icon-only buttons are a classic a11y failure: a screen reader announces "button" with no label. sr-only fixes it — text is rendered (so it is in the accessibility tree) but clipped to a 1×1 px box off-screen. The gold-check below asserts the exact computed box Tailwind emits. not-sr-only undoes it — the standard "skip to content" link pattern: sr-only focus:not-sr-only hides it until focused.

← visually just an ✕, but a screen reader announces "Close, button". The hidden <span class="sr-only">Close</span> is the label.
skip to main content ↑ a skip-link: invisible until you Tab to it, then it pops in. Click it (it won't navigate — demo only).

sr-only computed box: …

.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; }

4 · contrast-more & forced-colors — high-contrast fallbacks

contrast-more: layers extra weight only when the user asked for it. The card below upgrades from a faint border to a thick black one and bold text the instant prefers-contrast: more is on. forced-colors: is a separate concern: Windows High Contrast mode overrides your entire palette to a system color set — use it to fix the things that break (gradients disappear, borders need CanvasText, etc.).

Card with a faint border normally; thick black border + bold + inverted background when "Increase contrast" is enabled.

prefers-contrast: …

forced-colors: …

macOS: Accessibility → Display → Increase contrast. Windows High Contrast: Settings → Accessibility → Contrast themes.

intent → pattern

intentpatternwhy
animate without nausea class="motion-safe:animate-bounce" animation is absent for users with Reduce Motion — zero JS feature-detect
explicit still fallback motion-reduce:translate-y-0 override a hover/enter transform for motion-sensitive users
keyboard-only focus ring focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400 mouse clicks stay clean; Tab users get a visible ring (WCAG 2.4.7)
style a wrapper when a child is focused <label class="focus-within:border-cyan-400">…<input/></label> ring the whole field, not just the input — better perceived target
label an icon button <button><svg/><span class="sr-only">Delete</span></button> screen reader announces "Delete, button" — visible UI unchanged
accessible skip link class="sr-only focus:not-sr-only focus:absolute …" hidden until Tab focuses it, then it appears (WCAG 2.4.1)
boost contrast on demand contrast-more:border-2 contrast-more:border-black low-vision users get a stronger outline only when they asked
survive Windows High Contrast forced-colors:border-CanvasText restore borders that your gradient/transparent palette dropped