SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/dark-mode-variables

7 CSS Variable Dark Mode Systems

6 Pure CSS1 with JSPublished 2026-10-05Free · no sign-up

Seven patterns for building dark mode with CSS custom properties: tokens switched by prefers-color-scheme, the light-dark() function, a system, light and dark choice that is remembered, lighter surfaces for elevation, images that adapt, semantic tokens over a palette and shades derived with color-mix().

01. Colour Tokens with prefers-color-scheme

Dark modeVariables

Every colour lives in a custom property on :root, and a prefers-color-scheme: dark media query redefines the same properties. Components only ever use the variables, so they switch theme without a single extra rule of their own.

View code
<div class="dmv1"><article><h3>Invoice #2041</h3><p>Due 12 October</p><a href="#pay">Pay now</a></article></div>
.dmv1 { --bg: #ffffff; --fg: #101828; --muted: #475467; --line: #e4e7ec; --accent: #6941c6; --on-accent: #ffffff; }
@media (prefers-color-scheme: dark) { .dmv1 { --bg: #111727; --fg: #f1f5f9; --muted: #94a3b8; --line: #263048; --accent: #a78bfa; --on-accent: #1e1b4b; } }
.dmv1 article { width: 260px; max-width: 100%; box-sizing: border-box; padding: 18px; border: 1px solid var(--line); border-radius: 14px; background: var(--bg); color: var(--fg); font: 14px Inter, system-ui, sans-serif; }
.dmv1 h3 { margin: 0 0 4px; } .dmv1 p { margin: 0 0 14px; color: var(--muted); }
.dmv1 a { display: inline-block; padding: 8px 14px; border-radius: 8px; background: var(--accent); color: var(--on-accent); font-weight: 700; text-decoration: none; }
.dmv1 a:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

02. The light-dark() Function

Dark modelight-dark()

light-dark(#fff, #111) picks the first colour in light mode and the second in dark mode, based on color-scheme. With color-scheme: light dark on the root, each colour is declared once in one place, with no media query at all.

View code
<div class="dmv2"><p class="t">Both colours declared in one line each.</p><button>Primary action</button></div>
.dmv2 { color-scheme: light dark; width: 280px; max-width: 100%; box-sizing: border-box; padding: 18px; border: 1px solid light-dark(#e4e7ec, #334155); border-radius: 14px; background: light-dark(#ffffff, #0f172a); color: light-dark(#101828, #e2e8f0); font: 14px Inter, system-ui, sans-serif; }
.dmv2 .t { margin: 0 0 12px; }
.dmv2 button { padding: 9px 14px; border: 0; border-radius: 8px; background: light-dark(#1d4ed8, #93c5fd); color: light-dark(#ffffff, #0b1220); font: 700 14px Inter, system-ui, sans-serif; cursor: pointer; }
.dmv2 button:focus-visible { outline: 3px solid light-dark(#1d4ed8, #93c5fd); outline-offset: 2px; }

03. System, Light and Dark Choice

Dark modePreference

A three-way choice that respects the system setting by default and lets people override it. The choice sets a data-theme attribute and is remembered in localStorage; System removes the attribute so the media query decides again.

View code
<div class="dmv3"><fieldset><legend>Theme</legend><label><input type="radio" name="dmv3" value="system" checked> System</label><label><input type="radio" name="dmv3" value="light"> Light</label><label><input type="radio" name="dmv3" value="dark"> Dark</label></fieldset><p class="prev">Preview text in the current theme.</p></div>
.dmv3 { --bg: #fff; --fg: #101828; --line: #e4e7ec; width: 280px; max-width: 100%; box-sizing: border-box; padding: 16px; border: 1px solid var(--line); border-radius: 14px; background: var(--bg); color: var(--fg); font: 14px Inter, system-ui, sans-serif; }
@media (prefers-color-scheme: dark) { .dmv3:not([data-theme=light]) { --bg: #0f172a; --fg: #e2e8f0; --line: #334155; } }
.dmv3[data-theme=dark] { --bg: #0f172a; --fg: #e2e8f0; --line: #334155; }
.dmv3 fieldset { display: flex; gap: 12px; min-inline-size: 0; margin: 0 0 12px; padding: 0; border: 0; }
.dmv3 legend { margin-bottom: 6px; padding: 0; font-weight: 700; }
.dmv3 label { display: flex; gap: 4px; align-items: center; }
.dmv3 .prev { margin: 0; padding: 10px; border-radius: 8px; border: 1px dashed var(--line); }
var w = document.querySelector('.dmv3'), saved;
try { saved = localStorage.getItem('dmv3-theme'); } catch (e) {}
function apply(v) { if (v === 'light' || v === 'dark') w.dataset.theme = v; else delete w.dataset.theme; }
if (saved) { apply(saved); var r = w.querySelector('input[value=' + saved + ']'); if (r) r.checked = true; }
w.addEventListener('change', function (e) { apply(e.target.value); try { localStorage.setItem('dmv3-theme', e.target.value); } catch (er) {} });

04. Elevation with Lighter Surfaces

Dark modeElevation

In dark themes shadows barely show, so raised surfaces are made a little lighter instead: page, card and menu each get their own surface token. The three tokens step up in lightness, which keeps depth readable without heavy borders.

View code
<div class="dmv4"><div class="card"><b>Card on the page</b><div class="menu"><b>Menu on the card</b><span>Highest surface</span></div></div></div>
.dmv4 { --s0: #0b0f19; --s1: #151b2b; --s2: #202838; width: 300px; max-width: 100%; padding: 16px; box-sizing: border-box; border-radius: 14px; background: var(--s0); font: 13px Inter, system-ui, sans-serif; color: #cbd5e1; }
.dmv4 .card { padding: 14px; border-radius: 12px; background: var(--s1); }
.dmv4 .menu { display: grid; margin-top: 12px; padding: 12px; border-radius: 10px; background: var(--s2); box-shadow: 0 8px 20px -10px #000; }
.dmv4 b { color: #f8fafc; }

05. Images in Dark Mode

Dark modeImages

Bright images glare in a dark theme. A picture element can swap to a darker image with a prefers-color-scheme media source, and illustrations without a dark version can be dimmed slightly with a brightness filter on dark.

View code
<figure class="dmv5"><picture><source media="(prefers-color-scheme: dark)" srcset="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 140'%3E%3Crect width='300' height='140' fill='%231e293b'/%3E%3Ccircle cx='240' cy='40' r='18' fill='%23e2e8f0'/%3E%3C/svg%3E"><img alt="Night sky illustration in dark mode, sunny sky in light mode" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 140'%3E%3Crect width='300' height='140' fill='%23bae6fd'/%3E%3Ccircle cx='240' cy='40' r='22' fill='%23facc15'/%3E%3C/svg%3E" width="300" height="140"></picture><figcaption>Swaps with your system theme</figcaption></figure>
.dmv5 { width: 300px; max-width: 100%; margin: 0; font: 13px Inter, system-ui, sans-serif; color: #475467; }
.dmv5 img { display: block; width: 100%; height: auto; border-radius: 12px; }
.dmv5 figcaption { margin-top: 6px; }
@media (prefers-color-scheme: dark) { .dmv5 img { filter: brightness(.9); } .dmv5 { color: #94a3b8; } }

06. Semantic Tokens over a Palette

Dark modeDesign tokens

Two layers of variables: a palette (gray-50 to gray-900, brand-500) and semantic tokens (surface, text, border, accent) that point at palette entries. Dark mode only remaps the semantic layer, so the palette stays stable and components never mention a raw colour.

View code
<div class="dmv6"><button class="btn">Save</button><p>Components use --text, --surface and --accent only.</p></div>
.dmv6 { --gray-50: #f8fafc; --gray-200: #e2e8f0; --gray-700: #334155; --gray-900: #0f172a; --brand-300: #a5b4fc; --brand-600: #4f46e5;
  --surface: var(--gray-50); --text: var(--gray-900); --border: var(--gray-200); --accent: var(--brand-600); --on-accent: #fff; }
@media (prefers-color-scheme: dark) { .dmv6 { --surface: var(--gray-900); --text: var(--gray-50); --border: var(--gray-700); --accent: var(--brand-300); --on-accent: var(--gray-900); } }
.dmv6 { width: 280px; max-width: 100%; box-sizing: border-box; padding: 16px; border: 1px solid var(--border); border-radius: 14px; background: var(--surface); color: var(--text); font: 14px Inter, system-ui, sans-serif; }
.dmv6 p { margin: 10px 0 0; font-size: 13px; }
.dmv6 .btn { padding: 9px 16px; border: 0; border-radius: 8px; background: var(--accent); color: var(--on-accent); font: 700 14px Inter, system-ui, sans-serif; cursor: pointer; }
.dmv6 .btn:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

07. Derived Colours with color-mix()

Dark modecolor-mix()

Hover, subtle background and border shades are all derived from one --accent with color-mix(), so changing the accent for dark mode, or for a client brand, updates every shade at once instead of a dozen hand-picked hex values.

View code
<div class="dmv7"><span class="chip">Tag</span><button>Accent button</button><p class="note">Tint, border and hover come from one variable.</p></div>
.dmv7 { --accent: #0e7490; --surface: #ffffff; --text: #101828; width: 280px; max-width: 100%; box-sizing: border-box; padding: 16px; border-radius: 14px; background: var(--surface); color: var(--text); font: 14px Inter, system-ui, sans-serif; }
@media (prefers-color-scheme: dark) { .dmv7 { --accent: #67e8f9; --surface: #0f172a; --text: #e2e8f0; } }
.dmv7 .chip { display: inline-block; margin-right: 8px; padding: 4px 10px; border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent); border-radius: 999px; background: color-mix(in srgb, var(--accent) 12%, var(--surface)); color: color-mix(in srgb, var(--accent) 70%, var(--text)); font-weight: 700; font-size: 12px; }
.dmv7 button { padding: 8px 14px; border: 0; border-radius: 8px; background: var(--accent); color: var(--surface); font: 700 13px Inter, system-ui, sans-serif; cursor: pointer; }
.dmv7 button:hover { background: color-mix(in srgb, var(--accent) 85%, var(--text)); }
.dmv7 button:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 50%, transparent); outline-offset: 2px; }
.dmv7 .note { margin: 10px 0 0; font-size: 13px; color: color-mix(in srgb, var(--text) 70%, var(--surface)); }

About these CSS Variable Dark Mode Systems

Components use tokens only

Define colours as custom properties (--bg, --text, --accent) and never write raw colours in components. Dark mode is then just a second set of values for the same names.

Respect the system, allow an override

Default to prefers-color-scheme, offer System, Light and Dark, and store an explicit choice. The System option should hand control back to the media query.

light-dark() for small sets

With color-scheme: light dark, light-dark(a, b) picks a colour for each mode in one declaration, which is handy for components with a few colours.

Elevation by lightness

Shadows disappear on dark backgrounds. Give raised surfaces slightly lighter backgrounds so cards and menus still separate from the page.

Frequently asked questions

Should dark mode be pure black?

Usually not. Very dark greys reduce glare and make elevation easier to show.

Do I need to change images?

Bright photos and illustrations can glare; swap them with picture and a prefers-color-scheme source, or dim them slightly.

Does light-dark() work everywhere?

In all current major browsers; keep a fallback colour for older ones.