Modern Color
Prerequisites: Custom properties
CSS color has moved far beyond hex codes.
oklch()
oklch(L C H) describes a color by perceptual lightness (0–100%),
chroma (colorfulness, roughly 0–0.4), and hue (0–360°):
:root {
--blue-300: oklch(80% 0.1 250);
--blue-500: oklch(60% 0.15 250);
--blue-700: oklch(42% 0.13 250);
--green-500: oklch(60% 0.15 150);
}
Unlike HSL, equal L values look equally light across hues — HSL's
hsl(60 100% 50%) (yellow) looks much lighter than hsl(240 100% 50%)
(blue), though both say 50% lightness. That makes OKLCH ideal for
palettes and for predictable contrast. It can also express the wider
gamut of modern P3 displays; browsers map out-of-gamut colors to the
closest displayable one.
Supported everywhere current (Chrome and Edge 111, Firefox 113, Safari
15.4), like oklab(), lab(), lch(), and color(display-p3 ...).
color-mix()
.button:hover { background: color-mix(in oklab, var(--brand) 85%, black); }
.tint { background: color-mix(in oklch, var(--brand) 12%, white); }
.border { border-color: color-mix(in srgb, currentColor 30%, transparent); }
Mix two colors in a given color space; percentages say how much of each.
Relative color syntax
Derive a color from another by taking its channels apart:
.overlay { background: rgb(from var(--brand) r g b / 50%); }
.darker { color: oklch(from var(--brand) calc(l - 0.15) c h); }
.complement { color: hsl(from var(--brand) calc(h + 180) s l); }
from <color> exposes channel keywords (r g b, l c h, h s l) you
can reuse or compute with (Chrome and Edge 122, Firefox 128, Safari 18).
Checking what the browser computes
<style>
:root { --brand: rgb(0, 100, 200); }
.mix { color: color-mix(in srgb, var(--brand) 50%, white); }
.rel { color: rgb(from var(--brand) r g b / 50%); }
.ok { color: oklch(60% 0.15 250); }
</style>
<p class="mix">mix</p><p class="rel">rel</p><p class="ok">ok</p>
<script>
for (const p of document.querySelectorAll("p")) {
console.log(p.className, getComputedStyle(p).color);
}
</script>
mix color(srgb 0.5 0.696078 0.892157)
rel color(srgb 0 0.392157 0.784314 / 0.5)
ok oklch(0.6 0.15 250)
Browsers keep modern colors in their own color space when serializing,
so getComputedStyle returns color(srgb ...) and oklch(...) values
rather than converting everything to rgb().
Light and dark with light-dark()
:root {
color-scheme: light dark;
--surface: light-dark(#ffffff, #16181d);
--text: light-dark(#1f2328, #e6edf3);
}
[data-theme="light"] { color-scheme: light; }
[data-theme="dark"] { color-scheme: dark; }
color-schemetells the browser which schemes the page supports; it also switches native form controls and scrollbars.light-dark(a, b)picks a value based on the element's used color scheme. Settingcolor-schemeon an element lets a theme toggle override the system preference.- Supported in all major browsers since 2024.
System colors
Keywords like Canvas, CanvasText, LinkText, ButtonFace, and
Highlight follow the user's system theme — essential in Windows
forced colors mode, where the browser replaces your colors with the
user's.
Practice
Given --brand: oklch(55% 0.18 260), define a hover color 10% darker in
lightness and a background tint that is 10% brand mixed into white.
Show solution
:root {
--brand: oklch(55% 0.18 260);
--brand-hover: oklch(from var(--brand) calc(l - 0.1) c h);
--brand-tint: color-mix(in oklch, var(--brand) 10%, white);
}