Color, backgrounds and dark mode

Color notations including oklch, gradients and background layers, shadows versus borders, and a dark mode built on tokens.

Color notations that communicate intent

Every color notation can express the same pixel, so choose the one whose numbers mean something to a human. Hex is opaque; oklch exposes perceived lightness and hue, which makes it possible to adjust a palette deliberately.

NotationExampleWhen it helps
Hex#2f6fedCopying from a design tool, short and familiar
rgb() / rgba()rgb(47 111 237 / 0.6)Transparency without changing notation
hsl()hsl(220 76% 56%)Quick hue rotation, but lightness is perceptually uneven
oklch()oklch(56% 0.17 258)Building ramps: same L looks equally light across hues
color-mix()color-mix(in oklch, var(--brand) 15%, white)Deriving tints and shades from one token
:root {
  --brand: oklch(56% 0.17 258);
  --brand-soft: color-mix(in oklch, var(--brand) 15%, white);
  --brand-text: oklch(38% 0.14 258);
}

Because lightness in oklch is perceptual, a ramp built by changing only that channel looks even, while the same ramp in HSL tends to bunch up in the greens and blues.

Background layers, shadows and focus rings

The background shorthand accepts several comma-separated layers painted back to front, so an overlay gradient and a photograph are two layers of one declaration rather than two elements.

.hero {
  background:
    linear-gradient(180deg, rgb(0 0 0 / 0.55), rgb(0 0 0 / 0.1)),
    url("/img/hero.jpg") center / cover no-repeat;
}

.btn {
  border: 1px solid transparent;
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.08);
}

.btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}
  • The first layer listed paints on top; the last is the backdrop.
  • Gradients are images, so they obey background-size and background-position like any other layer.
  • Use the shorthand carefully — it resets every background sub-property you leave out.
  • A border is part of the box and affects layout; a shadow sits outside it and does not.
  • Use outline for focus rings: it never affects layout and follows the element's shape.

Dark mode without a second stylesheet

color-scheme: light dark tells the browser your page handles both themes, so form controls, scrollbars and the area behind the page match instead of staying stubbornly white.

:root {
  color-scheme: light dark;
  --surface: white;
  --text: #16181d;
}

@media (prefers-color-scheme: dark) {
  :root {
    --surface: #17181c;
    --text: #e9e9ec;
  }
}

body { background: var(--surface); color: var(--text); }

Override the same variables inside a [data-theme="dark"] rule to support an explicit user toggle. Because every component reads the variables, a toggle changes one attribute and the whole page follows.

⚠️
Dark mode is not light mode inverted. Pure white on pure black vibrates and smears on OLED screens — use an off-white text colour and a slightly raised surface. Check contrast in both themes and keep body text at 4.5:1 or better.

FAQ

Should a theme toggle be built in JavaScript?
A little. Default to the media query, let a stored preference or data-theme attribute override it, and set that attribute from an inline script in the head so the page never flashes the wrong theme.
Why do my gradients look banded?
Large, low-contrast ramps expose the limits of 8-bit color. Keep gradients shorter, interpolate in oklch, and add a very subtle noise texture if the banding is still visible.

Typography, fonts and fluid type CSS architecture, naming and design tokens

Last refreshed 2026-09-18.