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 / utility | compiles to | fires 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.
(if this is static, your OS has Reduce Motion ON — the variant honored it)
prefers-reduced-motion: …
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: …
.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.
<span class="sr-only">Close</span>
is the label.
sr-only computed box: …
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: …
intent → pattern
| intent | pattern | why |
|---|---|---|
| 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 |