System Mode
For my website, my goal was to keep things as simple as possible. No frameworks, just minimal HTML, CSS, and JavaScript.
Supporting both light and dark mode in 2026 is surprisingly simple. All it takes is a little CSS.
:root {
color-scheme: light dark;
}
This tells the browser that the page supports both light and dark color schemes. It allows the browser to adjust built-in UI elements like scrollbars and form controls. A user can change their system light or dark mode and watch the website respond. More explicitly, we can set these colors ourselves to better suit the website.
:root {
color-scheme: light dark;
background: blue;
color: red;
}
But then the colors are no longer dynamic based on the mode.
To solve this, we can use the light-dark() function.
:root {
color-scheme: light dark;
background: light-dark(blue, red);
color: light-dark(red, blue);
}
Now we have a blue background and red foreground in light mode, and a red background and blue foreground in dark mode.
User-Controlled Mode
This is great and all, but what if we want to let the user change the website color mode without having to change their system color mode? This will require some HTML (a button to click to change the mode), JavaScript (to set and apply the mode state), and a small amount of CSS.
The mode has three possible states: auto, light, and dark. Auto respects the current system color mode. This behaves like the initial basic example. The light and dark states will force the site to adopt the chosen state regardless of the system color mode. First, let’s make an HTML button that will change states when we click it. It should default to “auto” to respect the user’s system mode.
<label for="mode-select" class="visually-hidden">Mode</label>
<button id="mode-toggle" type="button" aria-label="Auto">
Auto
</button>
Next, we need a few JavaScript functions to apply, set, and cycle the mode state based on a data-mode variable.
const MODE_KEY = "mode";
const modes = ["auto", "light", "dark"];
const modeButton = document.getElementById("mode-toggle");
function applyMode(mode) {
if (mode === "auto") {
document.documentElement.removeAttribute("data-mode");
} else {
document.documentElement.setAttribute("data-mode", mode);
}
const label = mode[0].toUpperCase() + mode.slice(1);
modeButton.textContent = `${label}`;
modeButton.setAttribute("aria-label", `${label}`);
}
function setMode(mode) {
localStorage.setItem(MODE_KEY, mode);
applyMode(mode);
}
function nextMode(current) {
return modes[(modes.indexOf(current) + 1) % modes.length];
}
const initialMode = modes.includes(localStorage.getItem(MODE_KEY)) ? localStorage.getItem(MODE_KEY) : "auto";
applyMode(initialMode);
modeButton.addEventListener("click", function() {
const current = modes.includes(localStorage.getItem(MODE_KEY)) ? localStorage.getItem(MODE_KEY) : "auto";
const next = nextMode(current);
setMode(next);
});
Finally, we can add this CSS to set the color scheme based on the data-mode state.
html[data-mode="light"] {
color-scheme: light;
}
html[data-mode="dark"] {
color-scheme: dark;
}
User-Controlled Palettes
A color palette defines a coordinated set of colors for the interface, and can provide both light and dark variants. For example, the solarized color palette has both a light and dark mode variant. What if we allowed a user to set a color palette in addition to their color mode?
Let’s control the palette with a data-palette variable, and define the colors in CSS.
html[data-palette="solarized"] {
background: light-dark(#fdf6e3, #002b36);
color: light-dark(#657b83, #839496);
fill: light-dark(#657b83, #839496);
& a { color: #2aa198; }
& pre { background: light-dark(#eee8d5, #073642); }
}
html[data-palette="rose-pine"] {
background: light-dark(#faf4ed, #191724);
color: light-dark(#575279, #e0def4);
fill: light-dark(#575279, #e0def4);
& a { color: light-dark(#d7827e, #eb6f92); }
& pre { background: light-dark(#dfdad9, #403d52); }
}
html[data-palette="gruvbox"] {
background: light-dark(#fbf1c7, #282828);
color: light-dark(#3c3836, #ebdbb2);
fill: light-dark(#3c3836, #ebdbb2);
& a { color: #458588; }
& pre { background: light-dark(#ebdbb2, #3c3836); }
}
We can add a dropdown selection box to our HTML, but we’ll prepopulate it with available palettes using JavaScript.
<label for="palette-select" class="visually-hidden">Palette</label>
<select id="palette-select"></select>
Now we can add JavaScript functions to populate the dropdown and apply and set the palette.
const PALETTE_KEY = "palette";
const palettes = [
{ value: "system", label: "System" },
{ value: "solarized", label: "Solarized" },
{ value: "rose-pine", label: "Rosé Pine" },
{ value: "gruvbox", label: "Gruvbox" }
];
const paletteSelect = document.getElementById("palette-select");
// ---- Populate selection dropdown dynamically ----
palettes.forEach(palette => {
const option = document.createElement("option");
option.value = palette.value;
option.textContent = palette.label;
paletteSelect.appendChild(option);
});
function applyPalette(palette) {
if (palette === "system") {
document.documentElement.removeAttribute("data-palette");
} else {
document.documentElement.setAttribute("data-palette", palette);
}
paletteSelect.value = palette;
}
function setPalette(palette) {
localStorage.setItem(PALETTE_KEY, palette);
applyPalette(palette);
}
const initialPalette = palettes.map(t => t.value).includes(localStorage.getItem(PALETTE_KEY)) ? localStorage.getItem(PALETTE_KEY) : "system";
applyPalette(initialPalette);
paletteSelect.addEventListener("change", function() {
setPalette(this.value);
});
Ta-da!
With mode and palette separated, they can be freely combined.
A user might choose the Solarized palette in dark mode, or the Gruvbox palette in light mode.
Because we rely on light-dark(), each palette can define both variants cleanly.
To summarize the terminology:
- Mode controls luminance (light, dark, or auto)
- Palette controls color identity
- CSS handles rendering
- JavaScript only manages state
Try it out using the mode and palette selectors on the bottom of this page!