Home / Snippets / Layout /

Dark mode tokens

Define colors once with semantic tokens, override in prefers-color-scheme: dark. Add a [data-theme] selector for user-controlled toggling that overrides the OS preference.

Light mode

Card title

Secondary content and description text.

Action

Dark mode

Card title

Secondary content and description text.

Action
Widely Supported
layouttokensdark-modeno-js

Quick implementation

/* Light theme (default) */
:root {
  --bg:      oklch(0.98 0.005 250);
  --surface: oklch(1    0     0);
  --text:    oklch(0.15 0.02  260);
  --muted:   oklch(0.50 0.02  260);
  --border:  oklch(0.88 0.01  260);
  --accent:  oklch(0.52 0.22  265);
  --on-accent: oklch(0.98 0   0);

  color-scheme: light;
}

/* Dark theme — OS preference */
@media (prefers-color-scheme: dark) {
  :root {
    --bg:      oklch(0.13 0.02  260);
    --surface: oklch(0.19 0.02  260);
    --text:    oklch(0.93 0.01  260);
    --muted:   oklch(0.63 0.02  260);
    --border:  oklch(0.28 0.02  260);

    color-scheme: dark; /* tells browser to use dark scrollbars etc. */
  }
}

/* User override — beats OS preference */
[data-theme="light"] {
  --bg:      oklch(0.98 0.005 250);
  --surface: oklch(1    0     0);
  --text:    oklch(0.15 0.02  260);
  --muted:   oklch(0.50 0.02  260);
  --border:  oklch(0.88 0.01  260);
  color-scheme: light;
}

[data-theme="dark"] {
  --bg:      oklch(0.13 0.02  260);
  --surface: oklch(0.19 0.02  260);
  --text:    oklch(0.93 0.01  260);
  --muted:   oklch(0.63 0.02  260);
  --border:  oklch(0.28 0.02  260);
  color-scheme: dark;
}

/* Components use tokens only — never raw colors */
body { background: var(--bg); color: var(--text); }
.card { background: var(--surface); border: 1px solid var(--border); }

Prompt this to your LLM

Includes role, constraints, framework variants, and edge cases.

You are a senior frontend engineer implementing dark mode using
CSS custom property semantic tokens.

Goal: Create a complete dark mode token system that:
1. Defaults to light theme
2. Respects prefers-color-scheme: dark automatically
3. Allows user to override with a data-theme attribute

Include:
- color-scheme: light/dark property (controls browser chrome:
  scrollbars, form inputs, selection color)
- A JS snippet that reads/writes localStorage("theme") and sets
  data-theme on the document element
- Explain how to avoid the "flash of unstyled theme" on page load:
  inline script in head before body renders
- Note: accent color usually stays the same in both themes;
  only surfaces, text, and borders invert

Framework variants:
- Next.js: next-themes package usage
- SvelteKit: writable store + :global([data-theme="dark"])
- Astro: ViewTransitions and theme persistence

Return only CSS and JavaScript with inline comments.

color-scheme property

Setting color-scheme: dark on :root or html tells the browser to use dark variants for its own UI: scrollbars, form controls (checkboxes, selects, text inputs), text selection color, and the Canvas/CanvasText system colors. Without this, you can have a dark-background page with light-colored browser scrollbars — a jarring contrast. Always set color-scheme alongside your background token in the media query.

Preventing flash on page load

If you store the user's theme preference in localStorage and read it in JavaScript, there's a render window between the browser parsing your CSS (which defaults to light) and your JS running (which applies the dark class). During this window, the page flashes light. The solution: a small blocking inline <script> in <head> — before <body> renders — that reads localStorage and sets document.documentElement.setAttribute('data-theme', ...). Because it's inline and synchronous, it runs before the first paint.