In short
Modern CSS colors make palettes easier: oklch() gives even, predictable colors, color-mix() creates hover shades and tints, and currentColor keeps icons in sync. Why is oklch() easier than HEX and HSL? You already know HEX, RGB and HSL.
Modern CSS colors make palettes easier: oklch() gives even, predictable colors, color-mix() creates hover shades and tints, and currentColor keeps icons in sync.
Why is oklch() easier than HEX and HSL?
You already know HEX, RGB and HSL. HSL looks friendly, but it has a hidden problem: colors with the same "lightness" don't look equally light. hsl(60 100% 50%) (yellow) looks much brighter than hsl(240 100% 50%) (blue), even though both say 50%. That makes it hard to build a palette where every color feels balanced.
oklch() is built around how human eyes really see color. It has three parts:
- L – lightness, from
0%(black) to100%(white). It actually matches what you see. - C – chroma, how colorful it is.
0is grey; most real colors are between0.05and0.3. - H – hue, the color angle from 0 to 360 (around 30 is red, 140 green, 260 blue).
:root {
--blue: oklch(60% 0.18 260);
--green: oklch(60% 0.18 145);
--orange: oklch(60% 0.18 50);
--pink: oklch(60% 0.18 350);
}
.tag-blue { background: var(--blue); }
.tag-green { background: var(--green); }
.tag-orange { background: var(--orange); }
.tag-pink { background: var(--pink); }
We only changed the hue, and the four tags look equally light and equally strong. White text has similar contrast on all of them. Making a lighter version is simple too: raise L, for example oklch(85% 0.08 260) for a pale blue background.
Note: oklch can describe wide-gamut colors, which are more vivid than the old sRGB colors most websites use. Many modern phones and laptops can show them. On older screens, the browser automatically picks the closest color it can display, so very high chroma values are safe but may look the same as slightly lower ones.
Mixing colors with color-mix()
color-mix() mixes two colors, like mixing paint. You choose the color space (how to mix), the two colors, and how much of each.
:root {
--brand: oklch(55% 0.2 260);
}
.btn {
background: var(--brand);
color: white;
}
.btn:hover {
background: color-mix(in oklch, var(--brand), black 15%);
}
.btn:active {
background: color-mix(in oklch, var(--brand), black 30%);
}
.btn-soft {
background: color-mix(in oklch, var(--brand) 15%, white);
color: var(--brand);
}
.overlay {
background: color-mix(in srgb, var(--brand) 40%, transparent);
}
The button gets 15% darker on hover and 30% darker when pressed. The soft button uses a light tint: only 15% brand color, the rest white. The overlay is the brand color at 40% opacity, made by mixing with transparent. Change --brand once, and every shade updates. This is how you build a full hover/tint system from one variable.
Relative color syntax: changing one channel
Relative color syntax lets you take a color and change just one part of it. You write from and then a color, and you can use the letters l, c and h for its channels.
.card {
--accent: oklch(60% 0.2 145);
border-left: 4px solid var(--accent);
background: oklch(from var(--accent) 95% 0.03 h);
}
.card:hover {
border-color: oklch(from var(--accent) calc(l - 0.1) c h);
}
The card's background is a very pale version of the accent, with the same hue h. On hover, the border gets 10% darker (lightness in oklch goes from 0 to 1, so 0.1 is 10%), while chroma and hue stay the same. It works in all current major browsers.
currentColor for icons and borders
currentColor is a keyword that means "the value of this element's color property". It is old, but it is still one of the most useful color tools. When you change the text color, everything that uses currentColor follows.
<a class="icon-link" href="/download">
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
<path d="M8 1v9M4 6l4 4 4-4M2 14h12" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>
Download notes
</a>
.icon-link {
color: #2563eb;
border: 2px solid currentColor;
padding: 8px 12px;
border-radius: 8px;
display: inline-flex;
align-items: center;
gap: 8px;
text-decoration: none;
}
.icon-link:hover {
color: #dc2626;
}
The text, the border and the SVG arrow are all blue. On hover we change only color, and all three turn red together. Borders and outlines already default to currentColor, so border: 2px solid would work too, but writing it shows your intention.
Tip: Inline SVG icons that use
fill="currentColor"orstroke="currentColor"automatically match the text around them, in buttons, links and dark mode.
Common mistake: Writing
color-mix(red, blue)without theinpart. The newest spec makes the color space optional, but many browsers still require it and will ignore the whole line. Always write it, for examplein oklchorin srgb.
Read more on MDN's oklch() page.
Practice exercise
Create a palette from one variable --brand written in oklch. Using color-mix(), make a hover shade (darker), a pressed shade (even darker), a soft background tint, and a 50% transparent version. Then build an alert box with an SVG icon that uses currentColor, and switch the alert's color to make "success", "warning" and "error" versions.
Hint: for each alert type you only need one line, like .alert-error { color: oklch(55% 0.2 25); }, if the border, icon and title all use currentColor.
Key takeaways
oklch(L C H)gives perceptually even colors, making balanced palettes easy.- oklch can express wide-gamut colors; browsers adjust them for older screens.
color-mix(in oklch, color, black 15%)creates shades and tints from one variable.- Relative color syntax (
oklch(from var(--c) l c h)) changes one channel of a color. currentColormakes borders and SVG icons follow the text color.