Accessibility and CSS — focus states, motion, and contrast
A surprising share of real accessibility bugs live in CSS, not markup — a removed focus outline or an unconditional animation can make an otherwise perfectly semantic page unusable for a real group of real users.
3 min read
Never remove a focus indicator without replacing it with something equally visible
/* A genuinely common, genuinely damaging mistake */
:focus { outline: none; }
/* The fix — keep a clear, visible indicator, just styled to match the design */
:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}A visible focus indicator is how a keyboard-only user (someone who can't use a mouse, or is navigating efficiently without one) sees which element is currently active — remove it, and every interactive element on the page becomes genuinely unusable for that entire category of user, since there's no way to tell where keyboard input is currently going. :focus-visible (rather than plain :focus) is the modern refinement: it shows the indicator for keyboard navigation specifically, while suppressing it for a mouse click — addressing the actual complaint that motivated outline: none in the first place (an unwanted outline flash on mouse click) without removing the indicator keyboard users genuinely depend on.
prefers-reduced-motion: respecting a real, specific user setting
.hero-animation {
animation: slide-in 0.6s ease-out;
}
@media (prefers-reduced-motion: reduce) {
.hero-animation {
animation: none; /* respect the user's OS-level accessibility setting */
}
}Some users genuinely experience real physical symptoms (dizziness, nausea, migraines) from motion effects — vestibular disorders are a real, documented, and common enough condition that every major OS ships a system-level "reduce motion" setting, which prefers-reduced-motion: reduce lets a page detect and respect directly in CSS. This isn't a nice-to-have polish item; for the users this setting exists for, an unconditional animation is a genuine barrier to using the page at all, not just an aesthetic annoyance.
Color contrast: a measurable, testable requirement, not a subjective call
/* Fails WCAG AA for normal text (needs a 4.5:1 contrast ratio) */
.subtle-text { color: #999; background: #fff; } /* actual ratio: ~2.85:1 */
/* Passes WCAG AA */
.subtle-text { color: #666; background: #fff; } /* actual ratio: ~5.7:1 */WCAG (the Web Content Accessibility Guidelines) defines a precise, calculable minimum contrast ratio between text and its background — 4.5:1 for normal-size text at the common "AA" level — which is a genuinely measurable number, not a subjective design judgment, and browser DevTools' color picker typically shows the computed ratio directly while choosing a color. Low-contrast text is a real barrier for users with low vision, color blindness, or simply anyone viewing a screen in bright sunlight — a category of accessibility issue that's entirely preventable by checking the number before shipping a color pairing, rather than eyeballing whether text "looks readable enough."
aria-hidden and display: none/visibility: hidden are not interchangeable
.tooltip { display: none; } /* removed from the accessibility tree entirely — genuinely invisible to everyone */
.visually-hidden { /* the deliberate pattern for "visible to screen readers, invisible visually" */
position: absolute;
width: 1px; height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
}display: none (and visibility: hidden) removes an element from both the visual rendering and the accessibility tree — genuinely gone for every user, sighted or not. Content meant to be read by a screen reader but not shown visually (a common pattern: extra context for an icon-only button) needs the .visually-hidden pattern above instead — visually clipped to nothing, but still present in the accessibility tree, a real, different outcome from simply hiding the element with display: none.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why is `:focus { outline: none; }` without a replacement a genuinely damaging mistake?
2. How does `:focus-visible` improve on plain `:focus` for this problem?
3. What's the real difference between `display: none` and the "visually-hidden" CSS pattern (clip + absolute positioning)?