skip to content

In CSS, which media feature reports the user's light/dark appearance preference, and how do you use it to theme a page?

level: juniorimportance: should knowfreq 55%

answer

  1. a preference piped in from the OS
  2. two values only, no third state
  3. override the values, not every rule
  4. browser widgets need their own opt-in

basics

~20 s

The prefers-color-scheme media feature reports the operating system's appearance setting and matches either light or dark. Author one theme as the default, override the colour values inside @media (prefers-color-scheme: dark), and set color-scheme so browser-rendered UI follows too.

solid answer

~50 s

`prefers-color-scheme` is a media feature that exposes the user's system-level appearance preference, and it matches one of two values: `light` or `dark`. The usual pattern is to author one theme as the plain default — most teams write light — and then re-declare only the colour values inside `@media (prefers-color-scheme: dark)`, normally by reassigning a handful of custom properties on `:root` so the rest of the stylesheet never mentions a theme. I would also set `color-scheme: light dark` on `:root`, because that is the declaration that tells the browser to render form controls, scrollbars and the default canvas background in the matching palette — the media query only affects declarations I write myself. If the product also ships its own theme switch, the media query supplies the initial default and the explicit user choice overrides it.

code

css · 17 lines
css
:root {
  color-scheme: light dark;
  --surface: #ffffff;
  --text: #101418;
}

@media (prefers-color-scheme: dark) {
  :root {
    --surface: #101418;
    --text: #e8ecf1;
  }
}

body {
  background: var(--surface);
  color: var(--text);
}

go deeper

for a junior

Be ready to write the block from memory: default colours at the top, then @media (prefers-color-scheme: dark) re-declaring them. Say clearly that the two values are light and dark.

for a middle

Explain why the theme lives in custom properties re-assigned in one place, and why color-scheme is a separate declaration that governs user-agent-rendered widgets rather than your own rules.

for a senior

Show you have shipped this: contrast in the dark palette, shadows and borders that survive inversion, assets that assume a white background, and a user toggle layered over the system default without a flash of the wrong theme.

for a principal

Own the decision of whether dark mode is a token-level concern in the design system or a per-component one, and what that costs every future component; argue for a single semantic colour layer so themes never fan out across the codebase.

## What the feature actually reports `prefers-color-scheme` is a *user-preference media feature*: a condition you can test in an `@media` rule that describes the person and their environment rather than the screen's dimensions. It reports whether the user has asked their operating system (or, on some platforms, their browser) for a light or a dark appearance. It is not an ambient-light sensor, it does not know what time it is, and it does not know what colours your page currently uses — it is a single preference flag piped from the OS into CSS. Because it is a media feature, it is *live*. When the user flips their system appearance while your page is open, the query re-evaluates and the styles swap immediately, with no reload and no JavaScript. ## The values There are exactly two: `light` and `dark`. An earlier draft of the specification also defined `no-preference`, but it was removed, and browsers do not report it — if the user has expressed no preference, the platform simply reports `light`. So `@media (prefers-color-scheme: light)` and `@media (prefers-color-scheme: dark)` between them cover every user, which is why writing the light theme as the unconditional default and treating dark as the override is the conventional shape. ## The authoring pattern The pattern that scales is to keep every colour behind a named value and change only those values in the override block: ```css :root { color-scheme: light dark; --surface: #ffffff; --text: #101418; --border: #d8dde3; } @media (prefers-color-scheme: dark) { :root { --surface: #101418; --text: #e8ecf1; --border: #2a323b; } } body { background: var(--surface); color: var(--text); } ``` The important property of this shape is that there is exactly *one* `@media (prefers-color-scheme: dark)` block in the codebase. The alternative — a dark override next to every component — means every new component is a new opportunity to forget one, and the failure mode is dark text on a dark background. ## `color-scheme` is a separate job A very common bug is a page whose own surfaces go dark while its `<select>` dropdowns, checkboxes, scrollbars, and the blank canvas behind the page stay stubbornly light. That is because a media query only changes declarations *you* wrote; it cannot reach into user-agent-rendered widgets. The `color-scheme` property is what does that: ```css :root { color-scheme: light dark; } ``` This says "this page renders correctly in either scheme, so render your built-in UI in whichever one the user prefers." It also fixes the flash of white canvas before your stylesheet paints. ## Adding an explicit toggle If the product offers its own light/dark switch, the system preference becomes the *default* rather than the *decision*. The usual arrangement puts an attribute on the root element and lets a selector override the media query's values: ```css :root[data-theme="dark"] { --surface: #101418; --text: #e8ecf1; } ``` Because a selector on the root element wins over the same declarations set inside the media query at equal specificity by source order, placing the explicit-choice rules after the media query gives the user's choice priority while the query still handles everyone who has not chosen. ## `light-dark()` Newer stylesheets can collapse the two blocks with the `light-dark()` function, which takes two colour values and picks one according to the used colour scheme: `--surface: light-dark(#ffffff, #101418);`. It only works when `color-scheme` has been set on the element or an ancestor. It became available across the major browsers during 2024 (Firefox 120, Chrome 123, Safari 17.5), so a codebase that must support older browsers still writes the media-query form. ## What goes wrong in practice - **Only inverting background and text.** Borders, shadows, focus rings and disabled states all need attention; a pure-black shadow is invisible on a dark surface, and light-theme borders vanish. - **Images and illustrations baked for white.** Logos with white knockouts and screenshots with white chrome look broken; they need a dark counterpart or a container that keeps a light surface behind them. - **Insufficient contrast.** Pure white text on pure black is uncomfortable to read; dark themes usually use a near-black surface and a slightly dimmed foreground while still clearing contrast requirements. - **Assuming a reload is needed.** The query is live, so any state your page derives from it must be derived reactively rather than sampled once.

  • Why isn't the media query enough to make form controls and scrollbars look dark?
    Those are painted by the user agent, not by your declarations, so no rule of yours reaches them. The `color-scheme` property is the opt-in: `color-scheme: light dark` on `:root` tells the browser the page works in either scheme and lets it render its built-in widgets, scrollbars and the canvas background to match. Without it you get dark surfaces framed by light chrome.
  • A user has picked "dark" in your app's own theme switch while their OS is set to light. How do you make the app's choice win?
    Treat the media query as the default and the explicit choice as an override. Put the user's choice on the root element as an attribute or class and declare the same custom properties in a rule that comes after the media query block — equal specificity, later source order, so it wins. Persist the choice so it survives navigation, and fall back to the query when nothing is stored.
  • Does prefers-color-scheme require a page reload when the user changes their system setting?
    No. Media features are evaluated continuously, so flipping the OS appearance re-matches the query and repaints immediately. That is a reason to express the theme entirely in CSS rather than reading the preference once at startup and caching it — anything derived from a one-time sample goes stale the moment the user switches.

saying these in an interview costs you the question

  • Claims prefers-color-scheme has a no-preference value
  • Thinks the query detects ambient light or the time of day
  • Writes a dark override inside every component instead of one block
  • Forgets color-scheme, then blames the browser for light dropdowns
  • Says the page must reload for the new scheme to apply

context