Modern CSS Color Functions Explained

For a long time, CSS colors were mostly hex codes, RGB, and HSL.

Those formats still work and there is nothing wrong with them. But modern CSS now has several useful color functions that make it easier to create themes, color scales, lighter and darker versions, and more natural color changes.

The ones I find most useful are:

You do not need to replace every hex code in an old project. But if you are starting something new, these are worth knowing.

The formats we already know

A color can be written as a hex code:

.button {
  background: #3366ff;
}

Or RGB:

.button {
  background: rgb(51 102 255);
}

Or HSL:

.button {
  background: hsl(225 100% 60%);
}

These are all valid.

Modern CSS adds more choices rather than removing these.

Why oklch() is interesting

oklch() describes a color using three main values:

L = lightness
C = chroma
H = hue

The syntax looks like this:

color: oklch(65% 0.2 250);

The first value controls lightness.

The second controls how strong or colorful the color is.

The third is the hue angle.

The main advantage is that the lightness value is designed to match how bright a color looks to the human eye more closely than older models such as HSL.

You can read more about the model in the oklch() documentation.

Creating a color scale

Imagine you have one main brand color:

:root {
  --brand: oklch(60% 0.18 250);
}

You can make lighter and darker versions by changing the first number:

:root {
  --brand-100: oklch(95% 0.03 250);
  --brand-300: oklch(80% 0.10 250);
  --brand-500: oklch(60% 0.18 250);
  --brand-700: oklch(45% 0.16 250);
  --brand-900: oklch(25% 0.10 250);
}

Because the lightness value is more visually useful, the steps often feel more even than a similar HSL scale.

Alpha works too

Add a slash for transparency:

.overlay {
  background: oklch(20% 0 0 / 0.7);
}

This is similar to modern RGB syntax:

.overlay {
  background: rgb(0 0 0 / 0.7);
}

Mixing colors with color-mix()

One of my favourite newer functions is color-mix().

It does what the name suggests.

background:
  color-mix(in oklab, blue 70%, white);

That creates a mix of blue and white.

You can use a custom property:

:root {
  --brand: oklch(60% 0.18 250);
}

.button {
  background:
    color-mix(in oklab, var(--brand) 80%, white);
}

This is useful when you want to make hover colors without manually picking another hex value.

A simple hover state

.button {
  --button-color: oklch(60% 0.18 250);

  background: var(--button-color);
}

.button:hover {
  background:
    color-mix(
      in oklab,
      var(--button-color) 85%,
      black
    );
}

The hover color is based on the original color.

If the brand color changes later, the hover color changes with it.

Tints with transparency

You can also mix with transparent:

.notice {
  background:
    color-mix(
      in srgb,
      var(--brand) 12%,
      transparent
    );
}

This is useful for soft backgrounds behind alerts, tags, or badges.

Relative colors

Relative colors let you create a new color from an existing one.

For example:

:root {
  --brand: oklch(60% 0.18 250);

  --brand-light:
    oklch(from var(--brand) calc(l + 0.15) c h);

  --brand-dark:
    oklch(from var(--brand) calc(l - 0.15) c h);
}

This takes the original color and changes only its lightness.

The hue and chroma stay based on the original.

The MDN guide to relative colors shows that this syntax works across several modern color functions.

Why relative colors are useful

Without relative colors, a theme might look like this:

:root {
  --blue: #3366ff;
  --blue-light: #809cff;
  --blue-dark: #163ba8;
}

You had to choose and store every version yourself.

With relative colors:

:root {
  --blue: oklch(60% 0.18 260);

  --blue-light:
    oklch(from var(--blue) calc(l + 0.15) c h);

  --blue-dark:
    oklch(from var(--blue) calc(l - 0.15) c h);
}

Now the other colors are connected to the base color.

Light and dark themes with light-dark()

Another useful function is light-dark().

First tell the browser that the page supports both color schemes:

:root {
  color-scheme: light dark;
}

Then:

:root {
  --background: light-dark(#fff, #111);
  --text: light-dark(#111, #eee);
}

Use the variables normally:

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

The first value is used for light mode and the second for dark mode.

This can make a simple theme much easier to read than a long list of media-query overrides.

Mixing light-dark() with custom properties

:root {
  color-scheme: light dark;

  --page-bg:
    light-dark(
      oklch(98% 0 0),
      oklch(18% 0 0)
    );

  --page-text:
    light-dark(
      oklch(20% 0 0),
      oklch(92% 0 0)
    );
}

Now the whole theme lives in one place.

Modern RGB syntax

Even rgb() has become cleaner.

Old syntax:

color: rgba(0, 0, 0, 0.5);

Modern syntax:

color: rgb(0 0 0 / 0.5);

HSL can use the same pattern:

color: hsl(220 80% 50% / 0.5);

I prefer this style because the alpha value is clearly separate from the color channels.

Wide-gamut colors

Modern CSS can also use color spaces such as Display P3:

color: color(display-p3 1 0.2 0.1);

These colors can be more vivid on screens that support a wider color range.

I would not make this the first color feature to learn. For most normal web work, oklch() and color-mix() are more useful.

But it is good to know that CSS is no longer limited to the old sRGB range.

Use fallbacks when needed

For a site that needs older browser support, you can still provide a simple fallback first:

.button {
  background: #3366ff;
  background: oklch(60% 0.18 260);
}

A browser that does not understand oklch() ignores the second line and keeps the first.

You can also use @supports:

@supports (color: oklch(60% 0.18 260)) {
  .button {
    background: oklch(60% 0.18 260);
  }
}

Do not forget contrast

New color tools do not remove the need to check contrast.

A color may look nice but still be hard to read.

For important text, especially small text, check the final text and background combination rather than assuming a certain oklch() lightness will always be safe.

The same applies to generated colors from color-mix().

Automation is useful, but you still need to look at the result.

A small theme example

:root {
  color-scheme: light dark;

  --brand: oklch(62% 0.18 255);

  --background:
    light-dark(
      oklch(98% 0 0),
      oklch(17% 0 0)
    );

  --text:
    light-dark(
      oklch(20% 0 0),
      oklch(92% 0 0)
    );

  --border:
    color-mix(
      in oklab,
      var(--text) 18%,
      transparent
    );
}

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

a {
  color: var(--brand);
}

.card {
  border: 1px solid var(--border);
}

.button:hover {
  background:
    color-mix(
      in oklab,
      var(--brand) 85%,
      black
    );
}

That is already a flexible little color system without a long list of hand-picked values.

Which ones should you learn first?

If you do not want to learn every new color format, I would start with these:

oklch()      → better control of lightness and hue
color-mix()  → mix and tint colors
light-dark() → simple light and dark themes

Then look at relative colors once you want to build color scales from a base color.

You do not need to stop using hex codes. They are still simple and useful.

Modern CSS color functions are just extra tools, and some of them solve real problems that used to need preprocessors, design tokens, or manually chosen values.