Why oklch()
is the future of
colors in CSS

Andrey Sitnik, Evil Martians

😓

😍

rgb(186, 47, 226)
oklch(60% 0.26 317)

Front-end Principal at

@sitnikcode

The author of

@sitnikcode

CSS has evolved a lot

:root {
  --big-font: 18px; /* Variables */
}
.title {
  &.is-center { /* Nesting */
 
    /* Vertical centering */
    display: flex;
    align-items: center;
    justify-content: center;
  }
}

@sitnikcode

Changes are coming to colors too

.old {
  background: #bd89c2;
  background: hsl(295, 32%, 65%);
}

.new {
  background: oklch(70% 0.1 324);
}

@sitnikcode

Why do we need changes?

@sitnikcode

P3 colors

@sitnikcode

New screens show more colors
Many new Apple devices already support P3

Change 1

@sitnikcode

Design systems

Change 2

// old, for machine        // new, for human
   rgb()                      oklch()
   #hex                       lch()
                              hwb()

@sitnikcode

CSS Color 4 added better colors

Change 3

.error {
  background: oklch(from var(--accent) l c 20); 
}

@sitnikcode

Native color transform in CSS Color 5

Change 4

Section 2
What is oklch()

/* No comma */
rgb(1, 2, 3) → rgb(1 2 3)

/* No special function for alpha */
rgba(1, 2, 3, 0.5) → rgb(1 2 3 / 0.5)

/* % for alpha */
rgb(1 2 3 / 0.5) → rgb(1 2 3 / 50%)

/* The same for oklch() and all other color functions */
oklch(70% 0.1 330 / 50%)

@sitnikcode

New color syntax in CSS Color 4

@sitnikcode

OKLCH axes

Hue

Chroma

Lightness

oklch(L% C H / ALPHA)

0%

100%

0

0.4*

360°

* — technically, chroma is unlimited, not sRGB, P3 or Rec2020 > 0.4

L

C

H

oklch(100% 0    0)          /* white */

oklch(  0% 0    0)          /* black */

oklch( 60% 0    0)          /* gray */

oklch( 60% 0.2 20)          /* red */

@sitnikcode

OKLCH examples

@sitnikcode

oklch(75% 0.2 320)           /* light purple */

oklch(45% 0.2 320)           /* dark purple */

oklch(75% 0.1 320)           /* faded purple */

oklch(75% 0.2 140)           /* complementary green */

@sitnikcode

More OKLCH examples

Section 3
Why not rgb() or #hex?

Copy-and-paste programming

@sitnikcode

“It’s harder
to read code than to write it”

— Joel Spolsky

@sitnikcode

Readability: which is darker?

Problem 1

#8000ff
#80ff00

@sitnikcode

vs

Which has a contrast issue?

Problem 1

button.is-main {
  background: #8000ff;
  color: #ffffff;
}
button.is-secondary {
  background: #80ff00;
  color: #ffffff;
}

@sitnikcode

vs

The answer

Problem 1

button.is-main {
  background: #8000ff;
  color: #ffffff;
}
button.is-secondary {
  background: #80ff00;
  color: #ffffff;
}

Contrast issue

@sitnikcode

Compare with oklch()

Problem 1

button.is-main {
  background: oklch(53% 0.29 294);
  color: oklch(100% 0 0);
}
button.is-main {
  background: #8000ff;
  color: #ffffff;
}

Contrast issue

button.is-secondary {
  background: #80ff00;
  color: #ffffff;
}
button.is-secondary {
  background: oklch(89% 0.26 136);
  color: oklch(100% 0 0);
}

@sitnikcode

No random color picking anymore

Problem 2

Palette generation in design systems

Problem 2

--red-${i}: oklch(${60 * i / 100} ${0.3 * i / 100} 20)

Section 4
Why not hsl()?

Are hsl() and oklch() similar?

hsl()


Hue

Saturation

Lightness

oklch()


Lightness

Chroma

Hue

?

@sitnikcode

Which has a contrast issue?

button.is-main {
  background: hsl(270 100% 50%);
  color: hsl(0 0 100%);
}
button.is-secondary {
  background: hsl(90 100% 50%);
  color: hsl(0 0 100%);
}

@sitnikcode

vs

The answer

button.is-main {
  background: hsl(270 100% 50%);
  color: hsl(0 0 100%);
}
button.is-secondary {
  background: hsl(90 100% 50%);
  color: hsl(0 0 100%);
}
button.is-main {
  background: oklch(53% 0.29 294);
  color: oklch(100% 0 0);
}

Contrast issue

button.is-secondary {
  background: oklch(89% 0.26 136);
  color: oklch(100% 0 0);
}

@sitnikcode

The problem

hsl()


Hue

Saturation

Lie

@sitnikcode

The problem is not with a single pair

@sitnikcode

Section 5
Why is hsl() so bad?

@sitnikcode

HSL is a cylindrical color model

Hue

Lightness

Every hue has the same steps of lightness levels

@sitnikcode

Eye color sensitivity is complex

Image by Wikipedia

@sitnikcode

OKLCH is more accurate

Every hue has different steps of lightness

Hue

Lightness

HSL deforms space

How human eye sees colors

HSL deforms it to rectangle

HSL deforms artifacts visible in b/w

Section 6
Native color transformation


hsl(from hsl( 5  6  7)  h  s  l )

@sitnikcode

Color modification in CSS Colors 5

Just an example. Don’t use with hsl() in real projects.

                    /*  H  S  L */
hsl(from hsl( 5  6  7) 15 16 17)           → hsl(15 16 17)

hsl(from hsl( 5  6  7) h  s  17)           → hsl( 5  6 17)

hsl(from hsl( 5  6  7) h  s  calc(l + 10)) → hsl( 5  6 17)

@sitnikcode

How relative colors will work

Just an example. Don’t use with hsl() in real projects.

@sitnikcode

Just an example. Don’t use with hsl() in real projects.

/* A little lighter */
hsl(from var(--accent) h s calc(l + 10%))

/* Error from accent */
hsl(from var(--accent) 10 s l)

Better with Custom Properties


hsl(from var(--accent) h s calc(l - 10%))

vs

darken($accent, 10%)

@sitnikcode

Why not darken() from Sass?

darken(#7aae73, 40%)





       
       
darken(#40b1b2, 40%)

@sitnikcode

Same

darker

lighter

Because darken() uses HSL

In Native color transforms,
we can choose the color model

/* Bad: unpredictable */
hsl(from var(--accent) h s calc(l + 10%))

/* Good */
oklch(from var(--accent) calc(l + 10%) c h)

@sitnikcode

Use oklch() now…

button {
  background: oklch(53% 0.29 294);
}

button:hover {
  background: oklch(63% 0.29 294);
}

@sitnikcode

… to be familiar with oklch()
for native color transforms

button {
  --color: oklch(53% 0.29 294);
  background: var(--color);
}
button:hover {
  background: oklch(from var(--color) calc(l + 10%) c h);
}

@sitnikcode

Real magic with Custom Properties

button {
  background: var(--color);
  border: oklch(from var(--color) calc(l - 20%) c h);
}
button:hover {
  background: oklch(from var(--color) calc(l + 10%) c h);
}

button           { --color: var(--accent) }
button[disabled] { --color: var(--gray) }
button.is-delete { --color: var(--error) }

Button

Button

Button

@sitnikcode

Section 7
Wide-gamut P3 colors

Why does the sunset look different
if we have 16.7 million colors?

@sitnikcode

@sitnikcode

Screens show a subset of colors

All colors visible

by human

@sitnikcode

Modern screens show +30% colors

@sitnikcode

P3 is new retina: but how to use it?

button {
  background: #16f560;
}
@media (color-gamut: p3) {
  button {
    background: /* ? */;
  }
}

hex

rgb()

hsl()

P3 support

@sitnikcode

oklch() supports P3 colors

button {
  background: oklch(85% 0.25 147);
}
@media (color-gamut: p3) {
  button {
    background: oklch(85% 0.35 147);
  }
}

On non-P3 screens, the example will show only an emulation of the difference

@sitnikcode

oklch() supports beyond P3 colors

sRGB

P3

Rec2020

Far future

Chroma →

Section 8
oklch() problems

Is oklch() perfect?

CSS native support

The best for color transformation

Wide-gamut P3 colors

@sitnikcode

Not every oklch() is in sRGB

Problem 1

@sitnikcode

But browsers find the closest color

No hue changes

@sitnikcode

Ecosystem is still growing

Problem 2

OKLCH color space was created

2020

oklch() was added to Safari

2021

First OKLCH color picker

2022

Figma support

?

@sitnikcode

But ecosystem is growing fast

@sitnikcode

Section 9
Adding oklch() to project

oklch() browsers support

@sitnikcode

@sitnikcode

Polyfill: postcss-preset-env

.foo {
  color: oklch(60% 0.13 30);
}
.foo {
  color: rgb(193, 94, 80);
}

Step 1

@sitnikcode

.p3 {
  color: oklch(40% 0.27 35);
}

It also supports P3 colors

.p3 {
  color: rgb(131, 28, 0);
  color: color(display-p3 0.49 0.11 0);
}

@sitnikcode

Convert colors in OKLCH color picker

Step 2

@sitnikcode

Or by automatic script

npx convert-to-oklch ./src/**/*.css
.some {
  background: #bd89c2;
}

.some {
  background: oklch(70% 0.1 324);
}

@sitnikcode

Enforce oklch() by Stylelint

Step 3

// .stylelintrc
{
  "rules": {
    "function-disallowed-list": ["rgba", "hsla", "rgb", "hsl"],
    "color-function-notation": "modern",
    "color-no-hex": true
  }
}

@sitnikcode

Work together with a designer
to add P3 colors

Step 4

@sitnikcode

Add P3 colors to your landing page

button {
  background: oklch(85% 0.25 147);
}
@media (color-gamut: p3) {
  button {
    background: oklch(85% 0.35 147);
  }
}

@sitnikcode

Read our article about OKLCH

Step 5

Section 10
The result

@sitnikcode

Benefit 1: Readability

.warning {
  background: oklch(95% 0.1 100);
  color: oklch(75% 0.15 100);
}

@sitnikcode

Warning example

×

95% − 75% = 20% lightness difference

It is not enough for a good contrast

@sitnikcode

Benefit 2: P3 ready, rich colors on Apple

@sitnikcode

P3 is new retina

@sitnikcode

Benefit 3: Editable colors in code

@sitnikcode

button {
  background: oklch(75% 0.2 320);
  
  &:hover {
    background: oklch(70% 0.2 320);
                /* 5% darker */
  }
}

@sitnikcode

Benefit 4: Analyzable

@sitnikcode

if (l > 0.5) {
  return 'color: black'
} else {
  return 'color: white'
}

Black text is readable

White text is readable

@sitnikcode

Benefit 5: A11y palettes generation

@sitnikcode

Huetone uses OKLCH to generate palette with predictable contrast

@sitnikcode

Benefit 6: Colors understanding

@sitnikcode

@sitnikcode

Benefit 7: Understanding designers

@sitnikcode

Spain Russia

Spain Ukraine

> 2.5 billion €

0.3 billion €

Gas & fossil fuel in just the first 100 days of war. Now it still 10%.

Military, Financial and Humanitarian Aid

Ask Spanish politicians
to stop buying Russian gas.

“Fossil fuel imports from Russia in the first 100 days of the invasion” &
“Ukraine Support Tracker”

@sitnikcode

@sitnikcode

Subscribe

Why OKLCH is the future of colors in CSS

By Andrey Sitnik

Why OKLCH is the future of colors in CSS

  • 2,317