Skip to Content
Dark Mode With prefers-color-scheme and One Token File
Advanced
0 likes
Tutorials

Dark Mode With prefers-color-scheme and One Token File

Use one token file with light-dark(), add a saved toggle that does not flash, and check the colours in both themes.

Dark mode does not need a second stylesheet. One token file, a color-scheme declaration and a few lines of JavaScript cover the OS setting, a manual toggle and a saved choice.

Step 1: the token file

/* tokens.css */
:root {
  --blue-600: #034bff;
  --blue-300: #7aa2ff;
  --gray-900: #080b0e;
  --gray-600: #626976;
  --gray-50: #f2f8ff;
  --white: #ffffff;

  color-scheme: light dark;
  --surface: light-dark(var(--white), var(--gray-900));
  --text: light-dark(var(--gray-900), var(--gray-50));
  --text-muted: light-dark(var(--gray-600), #a9b1be);
  --accent: light-dark(var(--blue-600), var(--blue-300));
}

:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

color-scheme: light dark tells the browser to follow the OS. light-dark() picks its first or second value from that. The two data-theme rules force one scheme when the visitor chooses.

Step 2: use the tokens, nothing else

body { background: var(--surface); color: var(--text); }
a { color: var(--accent); }

Step 3: the toggle, saved between visits

<script>
  const saved = localStorage.getItem("theme");
  if (saved === "light" || saved === "dark") document.documentElement.dataset.theme = saved;
</script>
<link rel="stylesheet" href="tokens.css">
const root = document.documentElement;
const button = document.getElementById("toggle");
const isDark = () =>
  (root.dataset.theme ?? (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")) === "dark";

function sync() { button.setAttribute("aria-pressed", String(isDark())); }

button.addEventListener("click", () => {
  const next = isDark() ? "light" : "dark";
  root.dataset.theme = next;
  localStorage.setItem("theme", next);
  sync();
});
sync();

The first script goes in the <head>, before the stylesheet, so the saved theme is on the page before it is painted. Put it at the end and a dark-mode visitor sees a white flash on every load.

What it did in Chrome 154

first visit, OS light: rgb(255, 255, 255)   aria-pressed: false
after click:           rgb(8, 11, 14)       theme: dark   aria-pressed: true
after reload:          rgb(8, 11, 14)       saved theme: dark
first visit, OS dark:  rgb(8, 11, 14)       aria-pressed: true

Those are the body background colours. The click switched to dark and the choice survived a reload. A first visit with the OS set to dark started dark, with the button reporting that state.

⚠ Warning: localStorage can be blocked or full. Wrap the read and write in try/catch if your audience includes locked-down browsers, and let the OS setting be the fallback.

Check the colours

Test the contrast pairs for both themes. Muted text is 5.52:1 on white and 9.13:1 on the dark surface. The accent is 6.08:1 on white and 7.93:1 on dark. All of them pass 4.5:1 for normal text.

Common mistakes

  • Putting the theme script after the stylesheet or at the bottom of the body, which flashes the wrong theme.
  • Using the toggle button without aria-pressed, so screen reader users cannot tell the state.
  • Defining dark colours by hand for each component instead of changing tokens.

Related course: HTML and CSS Advanced.

About

Use one token file with light-dark(), add a saved toggle that does not flash, and check the colours in both themes.

Responsible
Mary Q
Last Update
08/20/2026
Reading Time
3 min
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.