The user preferences API
Browsers already know what your user wants — you just have to listen.
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.
/* 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).
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.
prefers-contrast
prefers-contrast lets users say "give me higher (or lower) contrast." It has values like more, less, no-preference, and custom.
: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.
: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.
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.
/* 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.
: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.
Four queries, one habit: ask the browser what the user wants. Last lesson, we slay some myths.