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.
ActionDark mode
Card title
Secondary content and description text.
ActionQuick 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.