Accessible Styling

CSS cannot fix inaccessible markup, but it can — and regularly does — create accessibility failures: removed focus indicators, contrast that fails WCAG, text that breaks under zoom, motion that causes harm. This lesson covers the styling decisions with explicit WCAG success criteria attached.

Focus indicators

Never remove the focus outline — naked

Keyboard users track their position by the focus indicator. Deleting it with outline: none is the most common CSS accessibility failure. Use :focus-visible to show a strong indicator for keyboard focus while leaving mouse clicks clean, and always replace, never just remove:

/* Wrong: keyboard users are now lost
.button:focus { outline: none; } */

/* Right: a visible, high-contrast ring for keyboard focus */
.button:focus-visible {
  outline: 3px solid #93c5fd;
  outline-offset: 2px;   /* the ring floats clear of the button's edge */
}
/* WCAG 2.4.7 (focus visible) + 2.4.13 (focus appearance, WCAG 2.2) */

Forced colors mode

/* Windows High Contrast Mode strips your colors: test with it on.
   Respect the user's palette where it matters, restore structure
   where meaning would otherwise be lost. */
@media (forced-colors: active) {
  .button { border: 1px solid ButtonText; }  /* keep the boundary visible */
}

Contrast and size

The numbers to hit

ContentWCAG AA minimumNotes
Body text4.5 : 1Measured against the actual background — gradients included
Large text (≥24px, or ≥18.66px bold)3 : 1The "large text" discount is narrower than most teams assume
UI boundaries (inputs, icons, focus)3 : 1WCAG 1.4.11 non-text contrast
Placeholder text4.5 : 1Placeholders vanish once typing starts anyway — label properly
/* A contrast-safe dark theme pair: measure it, do not eyeball it */
body { color: #e5e7eb; background: #0f172a; }  /* ~13:1 */
.muted { color: #94a3b8; }                     /* ~7:1  - passes for body text */
.faint { color: #64748b; }                     /* ~4.6:1 - body text only, never smaller */

Text that survives zoom

WCAG 1.4.4 requires the page to work at 200% zoom; 1.4.10 requires reflow at 320px width with 400% zoom. In practice: no fixed heights on text containers, no horizontal-scroll regions for prose, and font sizes in rem so zoom compounds sensibly.

Interactive targets

The 44px rule and its cousins

WCAG 2.5.8 (target size, minimum) requires a 24 × 24 CSS-pixel target; WCAG 2.5.5 (enhanced) requires 44 × 44. Touch is only part of the story — tremor and low-precision pointers benefit too. If the visual control must be small, expand its hit area transparently:

/* A small visual icon button with a full-size hit area */
.icon-btn {
  position: relative;
  width: 24px;
  height: 24px;
}
.icon-btn::after {
  content: "";
  position: absolute;
  inset: -10px;   /* the ::after IS the click target: 44px total */
}

Styling that hides meaning

Properties with side effects on assistive technology

/* display: none and visibility: hidden remove content from the
   accessibility tree as well as the screen - fine, when intentional. */

/* The screen-reader-only utility: visually gone, still announced */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

/* Danger: content rendered ONLY in ::before/::after is invisible
   to some assistive tech - never put meaningful text there. */

/* Danger: color alone never carries meaning (WCAG 1.4.1). Pair it: */
.error { color: #ef4444; }          /* not enough alone */
.error::before { content: "⚠ "; }   /* a non-color cue as well */

The prefers-* family

Media queries for human preferences

/* Reduced motion: kill travel, keep feedback (Animations lesson) */
@media (prefers-reduced-motion: reduce) { /* ... */ }

/* Dark and light themes: define tokens for each (Variables lesson) */
@media (prefers-color-scheme: dark) { :root { --bg: #0f172a; } }

/* The user asked for more contrast than your design ships */
@media (prefers-contrast: more) {
  .muted { color: #e5e7eb; }   /* stop muting text */
}

/* Reduced transparency: replace glassmorphism with solid fills */
@media (prefers-reduced-transparency: reduce) {
  .toolbar { background: #1e293b; backdrop-filter: none; }
}

Practice: an accessibility pass

The task

Open demo/quality/19_accessible_styling.html from the Lab Examples page, then audit one of your own pages with the checklist below and the WebAIM contrast checker (linked from References).

The checklist

  • Tab through the page with the mouse untouched: is every stop visible?
  • Run the text/background pairs through a contrast checker; fix every body-text failure.
  • Confirm no interactive target is smaller than 24 × 24 CSS pixels.
  • Zoom to 200% and then to 400%: no information is cut off or requires horizontal scrolling.
  • Verify no rule communicates by color alone.