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.

Advanced

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)?