A visitor who has switched their phone or computer to dark mode expects the websites they visit to notice, without having to hunt for a hidden toggle in a settings menu. The prefers-color-scheme media query answers exactly that need: it lets a stylesheet read the user's system preference and adapt its colors accordingly, with no JavaScript at all.
This feature is part of the level 5 media queries, a group of so called user preference queries that give CSS direct access to operating system settings. It is now supported by every modern browser and forms the backbone of most dark theme implementations on the web.
Definition
prefers-color-scheme is a CSS media query that detects whether the user has chosen a light or dark theme at the operating system or browser level. It accepts two main values: light and dark. A third value, no-preference, exists in the specification but is almost never returned by current browsers, which fall back to light when nothing has been set.
In practice, this media query is used inside an @media block, exactly the way a screen width would be targeted with a Breakpoint. The difference is that the condition being tested is no longer a dimension of the viewport, but an invisible system setting.
@media (prefers-color-scheme: dark) {
body {
background-color: #111111;
color: #f5f5f5;
}
}
@media (prefers-color-scheme: light) {
body {
background-color: #ffffff;
color: #111111;
}
}Why hardcoded colors everywhere become a problem
Writing a media query for every single color property quickly becomes unmanageable once a site has dozens of components. The recommended practice is to declare the media query only once, at the root of the document, and to redefine CSS custom property values there instead of properties directly. Every component then simply consumes those variables, without ever knowing which theme is active.
:root {
--bg-primary: #ffffff;
--text-primary: #111111;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #111111;
--text-primary: #f5f5f5;
}
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
}This structure has an extra benefit: it naturally prepares the ground for a manual toggle driven by JavaScript, which can override the same variables through an attribute or class on the html tag, without touching the rest of the code.
Adding a manual toggle on top of the system preference
Most sites that take dark mode seriously do not simply follow the system: they also offer a button to force a theme, whether or not the user agrees with their own system setting. This behavior is built by giving priority to an explicit attribute over the media query, using the order of rules in the Cascade.
:root {
--bg-primary: #ffffff;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #111111;
}
}
/* The manually chosen attribute wins over the system preference */
:root[data-theme="light"] {
--bg-primary: #ffffff;
}
:root[data-theme="dark"] {
--bg-primary: #111111;
}A small script then sets the data-theme attribute on html when the user clicks the button, and remembers the choice. As long as no choice has been made, the media query keeps driving the rendering.
A browser signal, not a design brief
prefers-color-scheme: dark means the user necessarily wants a fully black site with saturated bright colors. In reality, this media query only reports a display setting. A well designed dark theme keeps the same rules of readability, contrast and visual hierarchy as a light theme: it is not about mechanically inverting colors with a filter, but about redrawing a coherent, tested palette for a dark background.Interactions with images and media
Dark mode does not only concern background and text colors. An image with a white background can feel aggressive on a dark background, just as a chart with thin, light colored lines can disappear on a light background. The media query can also target an image's src inside a stylesheet, or be combined with the picture element to choose between two versions of a logo depending on the active theme.
| Value | Meaning | Typical use case |
|---|---|---|
light | The user prefers a light theme, or has not set anything | Default light palette |
dark | The user has enabled the system's dark mode | Dark palette, adapted images |
| No match | Browser that does not support the media query | The light theme applies by default |
Implementation best practices
For a clean result, it is best to test a component in both themes from the moment it is designed, rather than bolting dark mode on at the last minute. Contrast ratios must be checked separately for each theme, since a correct ratio in light mode guarantees nothing in dark mode. It is also worth adding a short transition on color properties, to avoid a jarring theme switch when the user changes their system setting mid session.
Frequently asked questions
It is not mandatory, but it is strongly recommended once the audience is broad. Many users enable dark mode for visual comfort or to save battery on an OLED screen, and a site that stays bright white in that context can feel neglected, even when the content itself is excellent.
Yes, prefers-color-scheme is supported by every current version of Chrome, Firefox, Safari and Edge, on both desktop and mobile. On older browsers that do not recognize it, the rule is simply ignored and the default light theme applies, which makes it entirely safe to use.
Yes, through window.matchMedia('(prefers-color-scheme: dark)'), which returns an object whose matches property reports the current setting and can also listen for live changes. This remains useful for syncing a JavaScript driven component, such as a Canvas chart, with the theme chosen by the CSS stylesheet.