Home / Articles / Color & Theming /

coloroklch

Relative color syntax: deriving colors dynamically

Define one base color and let CSS compute hover states, tints, shades, and transparent variants automatically.

The problem with manual color variants

Before relative color syntax, creating a hover state meant hand-picking a second color value. Need a lighter tint for a background? Another hand-picked value. A semi-transparent overlay? Yet another. Design systems would define dozens of static color tokens, and changing the brand color meant updating every single one.

Relative color syntax lets you derive new colors from an existing one using math on individual channels. You define the base once, and every variant is computed automatically.

/* Old way: every variant is a separate static value */
:root {
  --brand: oklch(0.6 0.2 265);
  --brand-hover: oklch(0.68 0.2 265);    /* manually picked */
  --brand-light: oklch(0.9 0.06 265);    /* manually picked */
  --brand-faded: oklch(0.6 0.2 265 / 0.2); /* manually picked */
}

/* New way: derive from a single base */
:root {
  --brand: oklch(0.6 0.2 265);
}
/* Variants computed where they are used — shown in the next section */

The syntax

Relative color syntax uses the from keyword inside a color function. You destructure the origin color into its channel variables, then use those variables (with optional math) to build the new color.

/* Syntax: oklch(from <origin> L C H / alpha) */

:root {
  --brand: oklch(0.6 0.2 265);
}

.button {
  background: var(--brand);

  /* Lighten by increasing L by 0.1 */
  &:hover {
    background: oklch(from var(--brand) calc(l + 0.1) c h);
  }

  /* Darken by decreasing L by 0.1 */
  &:active {
    background: oklch(from var(--brand) calc(l - 0.1) c h);
  }
}

/* l, c, h are channel keywords that reference
   the origin color's lightness, chroma, and hue */

The channel keywords l, c, and h are only available inside the relative color expression. They reference the corresponding channel of the origin color, and you can apply calc() to any of them.

Common transformations

Most color derivations fall into a few patterns. Here are the ones you will use most often:

:root {
  --base: oklch(0.6 0.2 265);
}

/* Tint: raise lightness, lower chroma */
.tint {
  background: oklch(from var(--base) calc(l + 0.25) calc(c - 0.12) h);
  /* Result: a soft, light version of the brand color */
}

/* Shade: lower lightness */
.shade {
  background: oklch(from var(--base) calc(l - 0.15) c h);
}

/* Desaturate: reduce chroma */
.muted {
  color: oklch(from var(--base) l calc(c - 0.1) h);
}

/* Transparent overlay: set alpha */
.overlay {
  background: oklch(from var(--base) l c h / 0.15);
}

/* Complementary: rotate hue 180° */
.complement {
  color: oklch(from var(--base) l c calc(h + 180));
}

You can combine multiple transformations in one expression — for example, lighten and desaturate simultaneously for a background tint.

Building interactive states

One of the most practical uses is building a complete button from a single color token. No Sass, no JavaScript, no extra custom properties.

.btn {
  --btn-color: oklch(0.6 0.2 265);
  background: var(--btn-color);
  color: oklch(0.98 0.01 265);
  border: 2px solid oklch(from var(--btn-color) calc(l - 0.08) c h);
  box-shadow: 0 2px 8px oklch(from var(--btn-color) l c h / 0.25);
  transition: background 0.15s, box-shadow 0.15s;
}

.btn:hover {
  background: oklch(from var(--btn-color) calc(l + 0.08) c h);
  box-shadow: 0 4px 16px oklch(from var(--btn-color) l c h / 0.35);
}

.btn:active {
  background: oklch(from var(--btn-color) calc(l - 0.06) c h);
  box-shadow: 0 1px 4px oklch(from var(--btn-color) l c h / 0.15);
}

.btn:focus-visible {
  outline: 3px solid oklch(from var(--btn-color) calc(l + 0.2) calc(c - 0.08) h);
  outline-offset: 2px;
}

Change --btn-color to any OKLCH value and every state adjusts automatically. This makes component theming trivial — pass a single custom property and the math handles the rest.

Working with other color spaces

Relative color syntax is not limited to OKLCH. You can convert between color spaces in the same expression — destructure from one space and output in another.

/* Convert a hex color to OKLCH and manipulate it */
.example {
  --legacy-brand: #4a7dff;

  /* 'from' converts automatically to the target space */
  background: oklch(from var(--legacy-brand) l c h);

  /* Lighten the hex value via OKLCH math */
  &:hover {
    background: oklch(from var(--legacy-brand) calc(l + 0.1) c h);
  }
}

/* Convert from OKLCH to sRGB for color-mix() compatibility */
.mixed {
  background: rgb(from oklch(0.6 0.2 265) r g b);
}

This interoperability means you can adopt relative color syntax incrementally — your existing hex or RGB tokens still work as origin colors.

Practical tips and gotchas

Relative color syntax is well supported in 2026 (Chrome 119+, Safari 18+, Firefox 128+), but there are a few things to keep in mind:

/* 1. Clamp to avoid out-of-range values */
.safe {
  /* L must stay between 0 and 1 */
  background: oklch(from var(--base) clamp(0, calc(l + 0.3), 1) c h);
}

/* 2. Chroma can't go negative */
.also-safe {
  color: oklch(from var(--base) l max(0, calc(c - 0.15)) h);
}

/* 3. currentColor works as an origin */
.icon {
  fill: oklch(from currentColor calc(l + 0.1) c h);
}

/* 4. Combine with color-mix() for percentage-based blending */
.subtle-bg {
  background: color-mix(
    in oklch,
    oklch(from var(--brand) l c h) 15%,
    oklch(0.13 0.02 260)
  );
}

Using clamp() and max() around channel math prevents the browser from clamping unpredictably. It also makes your intent explicit when reading the code later.