Embracing Native Dark Mode with the CSS color-scheme Property
May 03, 2025 | 3 minutesIn This Post
color-scheme
property on your root element to opt into the browser’s built-in light and dark styling for form controls, scrollbars, and other UI. Combine it with your own theme switches (e.g., a `data-theme` attribute or the `prefers-color-scheme` media query) to deliver a seamless dark mode experience—without writing extra CSS for native UI elements.
color-scheme
Matters
Why Browsers ship with polished default styles for scrollbars, form fields, focus rings, spellcheck underlines, and more—styles that automatically adapt to dark or light modes when you tell them what schemes you support. By declaring:
1:root { 2 color-scheme: light dark; 3}
you let the browser handle those defaults, so you don’t have to override everything with custom CSS. This not only saves time, but it also ensures that native controls look and behave consistently with user expectations.
Browser Support and System Defaults
The color-scheme
property has been widely supported since January 2022 across all major engines (Chrome, Firefox, Safari, and Edge). When you opt in, the browser will automatically flip between your light and dark defaults without extra code for:
- Canvas surfaces (e.g.,
<body>
background) - Scrollbars and interactive UI
- Form controls (buttons, inputs, selects)
- Spellcheck underlines and other UA-provided UI
To detect which scheme the user prefers so you can style your own components accordingly, you still use the prefers-color-scheme
media feature.
color-scheme
in Your Site
Implementing Here’s how I wired up my site’s native dark mode with minimal extra CSS:
1html { 2 /* light-dark() is from CSS Color Module Level 5—swaps colors based on scheme */ 3 background-color: light-dark(#ffffff, #2f2f2f); 4} 5 6html[data-theme="light"] { 7 /* Opt the page into light mode defaults */ 8 color-scheme: light; 9} 10 11html[data-theme="dark"] { 12 /* Opt the page into dark mode defaults */ 13 color-scheme: dark; 14}
The light-dark()
function makes it easy to pick between two colors based on the current scheme without any extra media queries required. Check out Google's web.dev blog for more on the light-dark()
function.
If you prefer the standard media-query approach, you can implement this in the following way instead:
1:root { 2 color-scheme: light dark; 3} 4 5@media (prefers-color-scheme: light) { 6 html { background-color: #fff; color: #000; } 7} 8 9@media (prefers-color-scheme: dark) { 10 html { background-color: #2f2f2f; color: #eee; } 11}
Toggling Themes with JavaScript
To give visitors explicit control of the color scheme or to respect a saved preference, you can toggle a data-theme
attribute on the <html>
tag. Here's a React component that will allow the user to toggle the theme.
1import React, { useState, useEffect } from 'react';
2
3export default function ThemeToggle() {
4 // 1. State holds current theme
5 const [theme, setTheme] = useState('light');
6
7 // 2. On mount, initialize from localStorage or system preference
8 useEffect(() => {
9 const saved = localStorage.getItem('theme');
10 const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
11 setTheme(saved || (systemDark ? 'dark' : 'light'));
12 }, []);
13
14 // 3. Whenever theme changes, apply to <html> and persist
15 useEffect(() => {
16 document.documentElement.setAttribute('data-theme', theme);
17 localStorage.setItem('theme', theme);
18 }, [theme]);
19
20 // 4. Listen for OS-level preference changes
21 useEffect(() => {
22 const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
23 const handler = (e) => setTheme(e.matches ? 'dark' : 'light');
24 mediaQuery.addEventListener('change', handler);
25 return () => mediaQuery.removeEventListener('change', handler);
26 }, []);
27
28 // Toggle handler
29 const toggleTheme = () => {
30 setTheme((current) => (current === 'dark' ? 'light' : 'dark'));
31 };
32
33 return (
34 <button id="theme-toggle" onClick={toggleTheme}>
35 Switch to {theme === 'dark' ? 'light' : 'dark'} mode
36 </button>
37 );
38}
Using a combination of local storage and an event listener on the prefers-color-scheme
media query ensures your theme switch is both persistent and responsive to OS adjustments.
Looking Ahead: Beyond Dark and Light
The CSS Color Module Level 6 is on the horizon, bringing custom property registrations and more advanced color functions. Soon you’ll be able to define your own scheme-aware variables directly in CSS with @property
and even explore experimental features like container-query-driven theming.
In the meantime, color-scheme
paired with light-dark()
lets you lean on the browser’s capabilities, reduce your maintenance burden, and deliver rock-solid dark mode with minimal effort. Happy theming!