Skip to main content
Syllabus
On this page

Dark Mode (prefers-color-scheme, light-dark())

Badar KhalilUpdated September 30, 2026 9 min read

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.

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-scheme is 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-scheme of the element, so you can force a section to dark with color-scheme: dark and 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 #000 with pure white text. Use a very dark gray such as #12141a and an off-white such as #e8eaf0 to 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: more and forced-colors: active with stronger borders and system colors.
  • Respect prefers-reduced-motion if you animate the theme change.
  • Do not rely on color only to show state.

9. Images, SVG and Media

  • Use the picture element 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: currentColor so 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 a media attribute, so the browser toolbar color can match each theme.
  • accent-color colors native checkboxes, radios and range inputs, and with light-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 .2s for 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.

Try it Yourself HTML
Output

Press Run to execute.

Try it Yourself HTML
Output

Press Run to execute.

Try it Yourself HTML
Output

Press Run to execute.

Try it Yourself HTML
Output

Press Run to execute.

Try it Yourself HTML
Output

Press Run to execute.

Try it Yourself HTML
Output

Press Run to execute.

Try it Yourself JAVASCRIPT
Output

Press Run to execute.

Exercise: Convert a Palette to light-dark()HTML

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.

Try it Yourself HTML
Output

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.

Exercise: Build a System, Light and Dark ToggleHTML

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.

Try it Yourself HTML
Output

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 mode

This is a self-check — compare your result with the expected output above.

Was this page helpful?