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*
0°
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 only 36% of all colors
All colors visible
by human
@sitnikcode
Modern screens show +25% 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
@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
Subscribe
Why OKLCH is the future of colors in CSS
By Andrey Sitnik
Why OKLCH is the future of colors in CSS
- 4,483