On this page
Dark Mode (prefers-color-scheme, light-dark())
CSS Dark Mode: prefers-color-scheme and light-dark()
Focus keyword: CSS dark mode. Dark mode is now an expected feature. Many users set their operating system to dark, and they expect your website to follow. In this lesson you will learn the prefers-color-scheme media query, the color-scheme property, the light-dark() function, how to build a manual theme toggle with three states, how to avoid the flash of the wrong theme, and how to design a dark palette that is readable and accessible.
Trending keywords covered in this lesson
CSS dark mode, prefers-color-scheme, light-dark() function, color-scheme property, meta color-scheme, dark mode toggle, dark mode localStorage, flash of incorrect theme, theme-color meta tag, dark mode CSS variables, data-theme attribute, system light dark theme, dark mode images, dark mode contrast, WCAG contrast ratio, color-mix, accent-color, prefers-contrast, forced-colors, prefers-reduced-motion, dark mode design tips.
1. How Dark Mode Works
The operating system or browser exposes the user's preference. CSS reads it with a media query, and the browser can also adapt built-in controls such as form fields and scrollbars. A good implementation has three parts: a default that follows the system, optional manual override by the user, and a palette designed for both themes.
2. The prefers-color-scheme Media Query
@media (prefers-color-scheme: dark) { ... } matches when the user prefers a dark theme. The other value is light. The recommended pattern is to define your colors as CSS variables on :root for light and redefine the same variables inside the dark media query. Components then use the variables and need no dark-specific rules.
3. The color-scheme Property and Meta Tag
The property color-scheme: light dark on :root tells the browser that your page supports both schemes. The browser then switches default colors for the canvas, text, form controls, scrollbars and the focus ring. You can also add the HTML tag <meta name='color-scheme' content='light dark'> in the head so the very first paint has the right background and avoids a white flash. Use color-scheme: only light only when you truly do not want the browser to adapt.
4. The light-dark() Function
light-dark(lightValue, darkValue) returns the first color in a light scheme and the second in a dark scheme. Example: background: light-dark(#ffffff, #12141a);. Important rules:
- It only works when
color-schemeis set, for example:root { color-scheme: light dark; }. Without it the function always returns the light value. - It accepts colors, so it is great for background, color, border-color and similar properties.
- It removes the need for a separate media query block for simple palettes.
- It follows the
color-schemeof the element, so you can force a section to dark withcolor-scheme: darkand the colors inside switch automatically. - It is supported by modern browsers. Add a media query based fallback if you need older ones.
5. Manual Toggle with Three States
Some users want a site setting that differs from their system. Offer three choices: System, Light and Dark. With light-dark() the CSS is short: keep :root { color-scheme: light dark; } for System, then add :root[data-theme='light'] { color-scheme: light; } and :root[data-theme='dark'] { color-scheme: dark; }. All colors written with light-dark() follow the forced scheme. If you use variables and media queries instead, repeat the dark variables inside the media query for System and inside the data-theme selector for the manual choice.
6. Saving the Choice and Avoiding the Flash
Save the user choice with localStorage. To avoid a flash of the wrong theme, read the saved value in a small inline script in the head, before the CSS is painted, and set the data-theme attribute on the html element immediately. Wrap storage calls in try and catch because storage can be blocked. Also listen for changes of the system setting with matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ...) when the user is in System mode.
7. Designing a Good Dark Palette
- Avoid pure black
#000with pure white text. Use a very dark gray such as#12141aand an off-white such as#e8eaf0to reduce glare. - Use elevation by lightness: higher surfaces such as cards and menus are slightly lighter than the page background, because shadows are hard to see on dark colors.
- Lower the saturation of bright colors and use lighter tints of your brand color for links and buttons.
- Do not just invert colors. Design each theme and check it.
- Keep semantic meaning: error stays red, success stays green, but adjust the tone for contrast.
8. Contrast and Accessibility
- WCAG requires a contrast ratio of at least 4.5 to 1 for normal text and 3 to 1 for large text and UI components, in both themes.
- Test focus rings, borders, disabled states and placeholder text in dark mode.
- Respect
prefers-contrast: moreandforced-colors: activewith stronger borders and system colors. - Respect
prefers-reduced-motionif you animate the theme change. - Do not rely on color only to show state.
9. Images, SVG and Media
- Use the
pictureelement with<source media='(prefers-color-scheme: dark)'>to load a different image for each theme, such as a logo with light text. - Soften bright photos in dark mode with
filter: brightness(.85). - For inline SVG icons use
fill: currentColorso they follow the text color. - Transparent PNGs with dark text may become unreadable, so test them on both backgrounds.
10. Other Useful Details
<meta name='theme-color'>accepts amediaattribute, so the browser toolbar color can match each theme.accent-colorcolors native checkboxes, radios and range inputs, and withlight-dark()it works in both themes.color-mix(in srgb, var(--primary) 20%, var(--bg))creates consistent tints and hover colors from your variables.- Use
transition: background-color .2s, color .2sfor a smooth switch, and disable it for reduced motion.
11. Common Mistakes
- Using light-dark() without setting color-scheme.
- Hard coding colors in components instead of variables or light-dark().
- Forgetting form controls, scrollbars and the page canvas, which stay light without color-scheme.
- Loading the theme in a script at the end of the page and causing a flash.
- Using pure black and pure white and creating eye strain.
- Testing only one theme for contrast.
- Ignoring images and logos that do not work on a dark background.
Key Takeaways
Set color-scheme to light dark, define colors with variables or light-dark(), follow the system by default and offer a saved manual override, set the theme early in the head to avoid a flash, design the dark palette with softer contrast and lighter elevation, and always check accessibility in both themes.
Press Run to execute.
Press Run to execute.
Press Run to execute.
Press Run to execute.
Press Run to execute.
Press Run to execute.
Press Run to execute.
Set color-scheme: light dark on :root and rewrite the background, text and card colors with light-dark(). Use a dark gray such as #12141a for the dark background, not pure black.
Press Run to execute.
Show expected output
The :root has color-scheme: light dark and body and card colors use light-dark(lightValue, darkValue)This is a self-check — compare your result with the expected output above.
Add three buttons (System, Light, Dark). System removes the data-theme attribute. Light and Dark set data-theme on the html element. Use color-scheme in CSS so light-dark() follows the choice, and save the selection in localStorage inside a try and catch block.
Press Run to execute.
Show expected output
CSS rules for :root[data-theme='light'] and :root[data-theme='dark'] that set color-scheme, plus a script that sets or removes data-theme and stores the modeThis is a self-check — compare your result with the expected output above.
Was this page helpful?