Accessibility Foundations · 7 / 8
lesson 7

The user preferences API

Browsers already know what your user wants — you just have to listen.

~ 13 min read·lesson 7 of 8
0 / 8

Operating systems ask users a lot of questions: do you prefer dark mode? Do animations make you nauseous? Is bandwidth precious? Is high contrast your default? Browsers expose those answers to your CSS as media features — a small set of @media queries that start with prefers-. Listening to them is one of the cheapest, most respectful accessibility wins you can ship.

This is what people mean when they say "design for the user's preferences, not for the average user."

prefers-reduced-motion

Some people get sick from motion. Vestibular disorders, migraines, motion sickness, and PTSD can all turn a perfectly innocent parallax or scale-up animation into a real-world headache. Most operating systems offer a "reduce motion" toggle. The browser exposes that as prefers-reduced-motion: reduce.

motion.css
/* Default — animate freely */
.card { transition: transform 240ms ease, opacity 240ms ease; }

/* If the user asked for less motion, calm everything down */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
  animation-duration: 0.01ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.01ms !important;
  scroll-behavior: auto !important;
}
}

The 0.01ms trick keeps the animation logically running (so JavaScript that listens for transitionend still fires) while making it visually instant. It's the safest one-liner you'll meet.

You can also do this on a per-component basis when you want some motion to remain (a tiny color fade is usually fine; a bouncing modal is not).

Tip

Reduced motion doesn't mean "no motion." Subtle fades and color transitions are often still appreciated. The thing to remove is large translation, scale, rotation, and parallax.

check your understanding
A user has 'Reduce Motion' on at the OS level. Your hero section auto-rotates between three slides every 4 seconds with a slide-left transition. What's the right behavior?

prefers-contrast

prefers-contrast lets users say "give me higher (or lower) contrast." It has values like more, less, no-preference, and custom.

contrast.css
:root {
--fg: #2a2a2a;
--bg: #fafafa;
--rule: #d8d8d8;
}

@media (prefers-contrast: more) {
:root {
  --fg: #000000;
  --bg: #ffffff;
  --rule: #000000;
}
}

If you build your design tokens as CSS variables (which Tailwind v4 themes already do), honoring this preference is often a matter of overriding three or four colors inside the @media query.

There's also a more aggressive feature called forced-colors, which kicks in when the OS forces its own palette (Windows High Contrast / Contrast Themes). When forced-colors: active, the browser overrides most of your colors with system colors and you should not fight it — instead, lean on system color keywords like Canvas, CanvasText, LinkText, and ButtonText.

prefers-color-scheme

The popular one — dark mode. prefers-color-scheme is light or dark based on the user's OS setting.

theme.css
:root {
color-scheme: light dark;          /* tells browser we support both */
--bg: #ffffff;
--fg: #1a1a1a;
}

@media (prefers-color-scheme: dark) {
:root {
  --bg: #111316;
  --fg: #f1ede4;
}
}

The color-scheme property at the top is an underused trick: it tells the browser to also style its built-in form controls and scrollbars to match. Without it, you get black scrollbars on a dark page.

If you have a manual light/dark toggle in your UI, layer it over the OS preference — usually with a data-theme="dark" attribute on <html> that overrides the media query when set.

check your understanding
You implement dark mode with a media query. A user on macOS in Dark Mode visits your site for the first time. What happens?

prefers-reduced-data

prefers-reduced-data is a newer, less universally supported preference: when set to reduce, the user wants you to use as little bandwidth as possible. Maybe they're on a metered connection, on a long flight, or in a region where data is expensive.

data.css
/* Default: large hero with cinematic background video */
.hero { background-image: url('/hero@2x.jpg'); }

@media (prefers-reduced-data: reduce) {
.hero { background-image: url('/hero-tiny.jpg'); }
.hero video { display: none; }
}

You can also load smaller images, skip autoplay videos, and pause analytics-heavy widgets when this preference is set. Browser support is partial — but progressive: if the browser doesn't understand it, your default kicks in, and that's fine.

Wiring up tokens

The big idea across all four preferences: don't sprinkle media queries through your components. Wire them into your design tokens once, and let the rest of your CSS read those tokens.

tokens.css
:root {
--bg: #ffffff;
--fg: #1a1a1a;
--motion-duration: 240ms;
}

@media (prefers-color-scheme: dark) {
:root { --bg: #111; --fg: #eee; }
}

@media (prefers-contrast: more) {
:root { --fg: #000; --bg: #fff; }
}

@media (prefers-reduced-motion: reduce) {
:root { --motion-duration: 0.01ms; }
}

.card {
background: var(--bg);
color: var(--fg);
transition: transform var(--motion-duration);
}

Now the whole UI bends to user preferences without each component knowing they exist.

check your understanding
You add a slick auto-playing background video. Which preference is most important to honor here?
check your understanding
Why is wiring user preferences into design tokens better than into individual components?

Four queries, one habit: ask the browser what the user wants. Last lesson, we slay some myths.

← prevnext lesson →
KeepLearningcertificate
for completing
Accessibility Foundations
0 of 8 read