Skip to content
AgileLogics
7 min read

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-scheme tells 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. Setting color-scheme on 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);
}