Command Palette

Search for a command to run...

Hectal
PHASE 11Advanced ~28 min· topic 7 of 8

Topic 11.7

Accessible CSS

In one line

CSS decides whether people can read, see where they are, and use a page comfortably: give text enough contrast, keep a clear focus indicator, hide text visually without hiding it from screen readers, survive forced-colours mode, and calm motion for people who ask for less.

Think of it like this

A railway station. Good stations have big signs with dark letters on a light background (contrast), a bright marker showing "You are here" (focus), announcements for people who can't see the signs (text for screen readers), signs that still work when the lights change (forced colours), and escalators that don't jerk (calm motion). Nothing about the trains changes; the station is usable by more people. CSS plays the same role for a page.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

WCAG
Web Content Accessibility Guidelines: the international standard for accessible web pages, with levels A, AA and AAA.
Contrast ratio
A number from 1:1 to 21:1 that measures how different two colours look in brightness.
Relative luminance
How bright a colour looks to the human eye, from 0 (black) to 1 (white), used to calculate contrast.
Focus indicator
The visible ring or highlight that shows which element the keyboard is on.
`:focus-visible`
A pseudo-class that matches a focused element when the browser thinks a visible focus ring is helpful, such as during keyboard use.
Screen reader
Software that reads the page aloud or in braille for people who can't see the screen.
Visually hidden
Present for screen readers but not shown on screen.
Forced colours mode
A system setting that replaces a page's colours with a small palette chosen by the user.
System colours
Colour keywords like Canvas and CanvasText that follow the user's system palette.
`prefers-reduced-motion`
A media feature that tells CSS the user asked the system to reduce animation.

Step by step

01Check the contrast

Pick colours, then check the ratio. Grey text is the most common failure: #999 on white is only 2.85:1. #767676 is the lightest grey that passes 4.5:1 on white.

style.csswhole filecss
body { color: #1f2937; background: #ffffff; } /* about 14.7:1 */
.muted { color: #6b7280; }  /* about 4.8:1: passes AA */

02Replace, never remove, the focus ring

Style :focus-visible with a clear outline. If you remove the default with outline: none, put a stronger style in its place.

style.csswhole filecss
:focus-visible {
  outline: 3px solid #2563eb;
  outline-offset: 2px;
}
html { scroll-padding-top: 4rem; } /* room for a sticky header */

03Hide text visually, not from screen readers

Give an icon-only button a text label inside a visually hidden span. The button's accessible name becomes "Close menu".

index.htmlwhole filehtml
<button class="icon-btn">
  <svg aria-hidden="true" ...></svg>
  <span class="visually-hidden">Close menu</span>
</button>

04Survive forced colours

Use borders and outlines for anything that must stay visible, and add a forced-colours block where you drew with backgrounds or shadows.

style.csswhole filecss
.toggle:focus-visible {
  box-shadow: 0 0 0 3px #93c5fd;   /* removed in forced colours */
  outline: 2px solid transparent;   /* appears in forced colours */
}
@media (forced-colors: active) {
  .toggle[aria-pressed="true"] { background: Highlight; color: HighlightText; }
}

05Calm the motion

Wrap big movement in a reduced-motion check, or set a calm default and add motion only for people who haven't asked for less.

style.csswhole filecss
@media (prefers-reduced-motion: no-preference) {
  .hero { animation: slide-in 600ms ease-out; }
}
Calm the motiondiagram
Rendering diagram…

Try it yourself

  1. 1

    Find the lightest passing grey

    In the first example, add ["#777777", "#ffffff"] to the pairs. Predict whether it passes AA (it's one step lighter than #767676), then run it.

  2. 2

    Break and fix the hidden text

    In the second example, change .visually-hidden to display: none. Predict what the console prints for display and what a screen reader would announce for the button now.

  3. 3

    Emulate forced colours

    In the third example, delete the @media (forced-colors: active) block, then emulate forced colours in DevTools. Predict what happens to the unchecked box.

Code & diagrams

A contrast checker New tab

This is the WCAG 2 formula: convert each channel to linear light, weight green the most (the eye is most sensitive to it), then compare the lighter and darker luminance with 0.05 added to each.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

#767676 on #ffffff 4.54:1 passes AA
#999999 on #ffffff 2.85:1 fails AA
#ffffff on #2563eb 5.17:1 passes AA
#000000 on #ffffff 21.00:1 passes AA
Focus rings and visually hidden text New tab

Press Tab: the button and link get a clear blue ring, but clicking with a mouse doesn't show it on the button (that is `:focus-visible` at work). The hidden span is still displayed (not `none`; `position: absolute` turns its `inline` into `block`), so screen readers announce "Close menu".

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

size: 1px 1px
display: block
button text for screen readers: Close menu
Reduced motion and forced colours New tab

In DevTools, open Rendering and emulate `prefers-reduced-motion: reduce` (the spinner slows down) and `forced-colors: active` (the custom box keeps a visible border, because shadows are removed in that mode).

Preview

Press Run to see the page.

Console

Nothing logged yet.

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Remove focus outlines everywhere

Add *:focus { outline: none; } to the second example.

terminal
$ Press Tab
── what you'll see ──
Focus moves (the browser still tracks it), but nothing on screen shows where it is. A keyboard user is lost.

Break #2

Hide a label with display: none

Hide the "Close menu" span with display: none instead of the visually hidden pattern.

terminal
$ Accessibility tree in DevTools
── what you'll see ──
The button's name is empty; a screen reader announces only "button".

Myth vs fact

Myth

Accessibility is only about HTML and ARIA.

Fact

CSS controls contrast, focus visibility, motion, zoom and reflow, visual order, and forced-colours behaviour; many WCAG failures are CSS bugs.

Myth

outline: none is fine if the design is clean.

Fact

Without a visible focus indicator the page fails WCAG 2.4.7 and is unusable by keyboard. Restyle the indicator; don't remove it.

Myth

prefers-reduced-motion means no animation at all.

Fact

It means reduce: remove large movement, parallax and autoplay, but small fades and essential progress indicators can stay.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    WCAG 2's contrast formula is simple but imperfect, especially for dark mode and thin fonts. APCA (Accessible Perceptual Contrast Algorithm) is being developed for a future WCAG 3 and models perception better. For conformance today, WCAG 2.x ratios are still the legal and audit standard.

  • ▸

    The old visually hidden pattern used clip: rect(0 0 0 0); clip is deprecated, so modern versions use clip-path: inset(50%), often with both for very old browsers. white-space: nowrap stops screen readers from reading the squeezed text one word per line.

  • ▸

    In forced-colours mode, forced-color-adjust: none opts an element out of the palette swap. Use it only for content where colour is the information (a colour swatch picker), and still respect the user's palette elsewhere.

  • ▸

    prefers-contrast: more (Phase 9) lets you strengthen borders and text for users who asked for more contrast, without going as far as forced colours. WCAG 2.2 also added Focus Not Obscured (2.4.11): a sticky header must not completely hide the focused element.

Remember this

  1. 1

    Contrast is how different the text colour is from its background, measured as a ratio from 1:1 (same colour) to 21:1 (black on white). The WCAG (Web Content Accessibility Guidelines) level AA asks for at least 4.5:1 for normal text and 3:1 for large text (at least 24px, or about 18.7px bold) and for non-text things people need to see: input borders, icons and focus indicators. Level AAA asks for 7:1 for normal text. DevTools shows the ratio in the colour picker, and the ratio is calculated from each colour's relative luminance (how bright it looks to the eye).

  2. 2

    Focus styles show keyboard users where they are. Browsers draw a default focus ring; outline: none without a replacement makes the page unusable by keyboard. Use :focus-visible, which matches when the browser decides a ring is helpful (keyboard navigation, text inputs), not after every mouse click: :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }. Make the ring at least 2px thick with 3:1 contrast against what's next to it, and make sure sticky headers don't cover the focused element (scroll-padding-top helps).

  3. 3

    Visually hidden text is text that screen readers announce but that isn't shown, for example "Close menu" on an icon-only button or "(opens in a new tab)" after a link. display: none and visibility: hidden hide content from everyone, including screen readers, so they are wrong here. The standard pattern shrinks the element to a 1px clipped box that is still in the accessibility tree: position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;. Many projects call this class .visually-hidden or .sr-only.

  4. 4

    Forced colours mode (Windows High Contrast, now called Contrast themes) replaces your colours with a small palette the user chose, to make the screen readable for people with low vision or light sensitivity. In this mode browsers remove box-shadow and background images used as colours, and swap all colours for system colours like Canvas, CanvasText, LinkText, ButtonText and Highlight. Anything you drew only with a background colour or a shadow (a custom checkbox, a focus ring made with box-shadow) can vanish. Detect it with @media (forced-colors: active) and draw with borders and outlines, which survive. A neat trick: outline: 2px solid transparent is invisible normally but becomes visible in forced colours.

  5. 5

    Motion can make some people dizzy or sick (vestibular disorders) and distracts others. Operating systems have a reduce motion setting, and CSS reads it with @media (prefers-reduced-motion: reduce). In that mode, remove large movements (parallax, sliding, zooming) and autoplaying animation, or replace them with a fade. Animations that pass information (a loading spinner) can stay but should be slower or simpler. Phase 10 covers the animation side.

  6. 6

    A few more CSS habits matter. Size text in rem so it follows the user's font-size setting, and make sure the page works at 200% zoom and when text spacing is increased. Don't use colour alone to carry meaning (add an icon or text to red errors). Keep touch targets at least 24×24 CSS pixels (WCAG 2.2) and preferably 44×44. And remember that CSS can change visual order (order, grid-area, flex-direction: row-reverse) without changing the reading and tab order, which follows the HTML; keep them matching.

Explain it without notes

01

What contrast ratios does WCAG AA require, and for what?

02

Why is outline: none harmful, and how do you style focus properly?

03

How do you hide text visually but keep it for screen readers? Why not display: none?

04

What is forced colours mode, and how should CSS handle it?

05

How should CSS respond to prefers-reduced-motion?

Practice

01

Write a :focus-visible style for buttons that also stays visible in forced colours mode, even though the normal design uses a box-shadow ring.

02

Write CSS that turns off a .carousel slide animation for users who prefer reduced motion.

03

Write a .visually-hidden class and use it to label a search button that only shows a magnifying-glass icon.

Trade-offs

  • ↔

    Higher contrast limits the palette (pale greys and pastel buttons are out), but it makes text readable for everyone, including people in bright sunlight.

  • ↔

    Custom-styled controls look on-brand but must be rebuilt for focus, forced colours and states that native controls handle for free; prefer styling native controls (accent-color, appearance) where you can.

Done when you can

  • I know the AA contrast ratios and can check them in DevTools.

  • I style :focus-visible with a clear outline and never remove focus without a replacement.

  • I can write the visually hidden pattern and explain why display: none is wrong.

  • I can test and fix a page in forced colours mode.

  • I reduce motion for users who prefer it.

  • I size text in rem and keep visual order matching the HTML order.