Modern CSS color functions
color-mix(), light-dark(), and relative color let CSS derive whole palettes and theme by scheme from one source color — no preprocessor needed.
In one line: modern CSS color functions let the browser compute your palette — blend two colors with color-mix(), branch on scheme with light-dark(), and derive variants with relative color — so a theme lives in one source value, not a wall of hand-picked hexes.
What it is
A design system used to ship every shade as a literal: --brand-500, --brand-600, a hover, an active, a subtle background — each a separate hex someone eyedropped and now has to maintain. Change the brand color and you re-pick them all.
Modern CSS computes them instead:
color-mix(in oklch, A, B)blends two colors in a chosen color space, by percentage.color-mix(in oklch, var(--brand) 85%, black)is a darker brand with no new hex.light-dark(lightVal, darkVal)returns its first or second argument based on the activecolor-scheme— light/dark theming with no media query and no extra class.- Relative color syntax (
oklch(from var(--brand) calc(l - 0.1) c h)) takes a color apart and rebuilds it, so you can shift just the lightness or hue.
Why it matters
One source of truth. The palette becomes a function of --brand; change that variable and every derived hover, border, and tint moves with it — in sync, by construction. That is what preprocessors (Sass darken()) gave us, except now it is live in the browser, responds to custom properties at runtime, and themes can change without a rebuild.
Working in oklch matters too: mixes and lightness steps are perceptually even, so a 10% darker step looks 10% darker across hues, instead of the muddy midpoints sRGB blending produces.
See it
Tweak it3
Drag the mix amount to blend the brand color toward green and watch the swatch update, or switch to light-dark() and flip the scheme to see the same declaration resolve to a different value.
How it works
color-mix() takes a color space (in oklch is the good default) and two colors, each with an optional percentage; the percentages are the weights. Leave them off and it is a 50/50 mix.
light-dark() only works when the element (or :root) opts in with color-scheme: light dark. The browser then picks the first value under a light scheme and the second under dark — driven by the OS setting or an explicit color-scheme on an ancestor, so a theme toggle is just changing that one property.
Relative color syntax destructures a color into channel keywords (l c h for oklch, r g b for sRGB) you can feed through calc(), then reassembles it — ideal for "the same hue, one step lighter."
Build it
:root {
color-scheme: light dark;
--brand: oklch(0.62 0.19 265);
/* Derived states — no new hexes to maintain. */
--brand-hover: color-mix(in oklch, var(--brand) 88%, black);
--brand-subtle: color-mix(in oklch, var(--brand) 12%, transparent);
--brand-lift: oklch(from var(--brand) calc(l + 0.08) c h);
/* Surfaces and text follow the system scheme automatically. */
--surface: light-dark(#ffffff, #0b1220);
--text: light-dark(#0b1220, #e5e7eb);
}
.button {
background: var(--brand);
color: var(--text);
}
.button:hover {
background: var(--brand-hover);
}
@supports not (color: light-dark(#000, #fff)) {
:root {
--surface: #ffffff;
--text: #0b1220;
}
}Make it yours
Use the controls beside the demo above to change function, mix amount, and dark scheme — each change updates the example live.
Experiment in the playground- Move the mix amount to 0% and 100% — the endpoints are the two input colors; everything between is computed.
- Switch to
light-dark()and flip the scheme: one declaration, two resolved values, zero media queries. - Imagine changing only
--brand: every mix and relative-color derivative shifts with it, in sync.
Reproduce it with an LLM
Reproduce it with an LLM
You are a senior front-end engineer building a small theme. From a single `--brand` color, derive hover/active/subtle variants using `color-mix(in oklch, var(--brand) X%, white|black)` instead of hand-picked hexes, so the whole scale stays in sync when the brand color changes. Set `color-scheme: light dark` and use `light-dark(<lightval>, <darkval>)` for surfaces and text so the theme follows the system scheme with no media query or extra class. Ensure text/background pairs meet WCAG AA contrast, and add an `@supports` fallback for `light-dark()`. Return only the CSS with a tiny HTML demo (a button and a card).
Pitfalls & accessibility
Related
Design tokens
Named design decisions — color, space, type, radius, motion — stored once, referenced everywhere. Inspect Motif's own tokens and the three-tier model.
Color & contrast
Color carries mood and meaning, but contrast carries legibility. Learn the WCAG ratios, how to hit them, and why OKLCH makes accessible palettes easier.