What is CSS OKLCH

OKLCH is a color model you can use in CSS via the oklch() color function. It’s designed to be more perceptually uniform than older formats, meaning that when you change the numbers, the visual change is more consistent and predictable. In other words: it tries very hard to make “small code tweaks” feel like “small visual tweaks”.

In CSS, you’ll most often use OKLCH the same way you use other colors: with properties like color, background, border-color, box-shadow, and gradients. The “OKLCH property” people talk about is usually just shorthand for “using OKLCH values in color-related properties”. The actual CSS feature is the oklch() function.

.demo {
  background: oklch(72% 0.18 250);
}
  
.demo {
  background: oklch(72% 0.18 130);
}
  
.demo {
  background: oklch(72% 0.06 250);
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 980px;
  padding: 13px;
  display: grid;
  gap: 14px;
  font-family: system-ui, Arial, sans-serif;
}

.demo {
  border: 3px solid #111;
  border-radius: 18px;
  padding: 18px;
  box-shadow: 0 12px 0 #111;
  display: grid;
  gap: 10px;
  align-content: start;
}

.demo h3 {
  margin: 0;
  font-size: 18px;
}

.demo p {
  margin: 0;
  max-width: 62ch;
  line-height: 1.4;
}

.pill-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 6px;
}

.pill {
  background: #fff;
  border: 2px solid #111;
  border-radius: 999px;
  padding: 8px 12px;
  font-size: 13px;
}

.code {
  background: rgba(255, 255, 255, 0.75);
  border: 2px solid rgba(0, 0, 0, 0.35);
  border-radius: 12px;
  padding: 10px 12px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px;
  line-height: 1.35;
}
  

OKLCH as a “normal CSS color”

Click the snippets above. We keep the same “lightness” value, then change hue or chroma to see how it affects the result.

background border-color text color shadow
oklch(L C H)
L = lightness
C = chroma
H = hue

CSS OKLCH function syntax

The basic syntax looks like this:

  • oklch(L C H)
  • oklch(L C H / A) (same thing, with alpha)

Where:

  • L (Lightness): usually a percentage like 70%. Think “how bright”.
  • C (Chroma): a number like 0.12. Think “how colorful / intense”.
  • H (Hue): an angle like 210 (degrees). Think “which color family”.

It’s very similar to HSL in spirit (lightness + colorfulness + hue), but the underlying math is different and the results tend to be more consistent when you tweak values.

Interactive OKLCH sliders

Let’s make OKLCH feel less abstract by giving you three sliders (Lightness, Chroma, Hue). Move them and watch how the color changes.

.swatch {
  --l: 72;
  --c: 0.16;
  --h: 250;
  background: oklch(calc(var(--l) * 1%) var(--c) var(--h));
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 980px;
  padding: 18px;
  display: grid;
  gap: 14px;
  font-family: system-ui, Arial, sans-serif;
}

.grid {
  display: grid;
  gap: 12px;
}

.swatch {
  border: 3px solid #111;
  border-radius: 18px;
  min-height: 220px;
  box-shadow: 0 12px 0 #111;
  display: grid;
  align-content: end;
  padding: 16px;
}

.panel {
  background: rgba(255, 255, 255, 0.82);
  border: 2px solid rgba(0, 0, 0, 0.35);
  border-radius: 14px;
  padding: 12px 12px;
  display: grid;
  gap: 6px;
}

.panel h4 {
  margin: 0;
  font-size: 14px;
}

.panel p {
  margin: 0;
  font-size: 13px;
  line-height: 1.35;
  max-width: 60ch;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
  

Your current OKLCH

oklch(L C H) where L is a percentage, C is chroma, and H is degrees.

CSS OKLCH opacity and alpha

OKLCH supports transparency the same way modern rgb() and hsl() do: you add a slash and an alpha value:

  • oklch(70% 0.15 250 / 0.7)
  • oklch(70% 0.15 250 / 70%)

That last piece (after /) is the alpha channel. It can be a number from 0 to 1, or a percentage.

Interactive alpha slider

This playground uses one slider to change the alpha value while keeping the color constant. Notice how the background pattern “shows through” more as alpha approaches 0.

.card {
  --a: 0.85;
  background: oklch(68% 0.18 25 / var(--a));
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 980px;
  padding: 18px;
  font-family: system-ui, Arial, sans-serif;
}

.stage {
  border: 3px solid #111;
  border-radius: 18px;
  box-shadow: 0 12px 0 #111;
  padding: 16px;
  background:
    repeating-linear-gradient(
      45deg,
      #f2f2f2 0 12px,
      #e6e6e6 12px 24px
    );
  display: grid;
  gap: 12px;
}

.card {
  border: 2px solid rgba(0, 0, 0, 0.35);
  border-radius: 16px;
  padding: 14px;
  display: grid;
  gap: 8px;
}

.card h4 {
  margin: 0;
  font-size: 16px;
}

.card p {
  margin: 0;
  line-height: 1.45;
  max-width: 60ch;
}

.mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px;
  background: rgba(255, 255, 255, 0.75);
  border: 1px solid rgba(0, 0, 0, 0.3);
  border-radius: 12px;
  padding: 10px 12px;
}
  

OKLCH with alpha

Alpha controls transparency. 1 is fully opaque, 0 is fully transparent.

background: oklch(68% 0.18 25 / A);

CSS OKLCH vs HSL

HSL is popular because it’s easy to read: hue, saturation, lightness. The catch is that HSL’s “lightness” and “saturation” don’t map cleanly to how humans perceive brightness and intensity. So you can set a bunch of colors to lightness: 60% and they won’t necessarily feel equally bright.

OKLCH aims to fix that by using perceptual math (based on the OKLab color space), so that equal “L” values are much closer to equal perceived lightness.

Same “lightness” comparison

This demo shows a row of HSL colors and a row of OKLCH colors. Both rows keep their “lightness” concept constant (HSL uses l, OKLCH uses L), but OKLCH usually looks more even across hues.

.row-hsl .chip {
  background: hsl(var(--h) 90% 60%);
}
  
.row-oklch .chip {
  background: oklch(70% 0.17 var(--h));
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 980px;
  padding: 18px;
  display: grid;
  gap: 14px;
  font-family: system-ui, Arial, sans-serif;
}

.card {
  border: 3px solid #111;
  border-radius: 18px;
  box-shadow: 0 12px 0 #111;
  padding: 16px;
  display: grid;
  gap: 12px;
  background: #fff;
}

.card h4 {
  margin: 0;
  font-size: 16px;
}

.card p {
  margin: 0;
  line-height: 1.45;
  max-width: 70ch;
}

.rows {
  display: grid;
  gap: 12px;
}

.row {
  display: grid;
  gap: 8px;
}

.label {
  font-weight: 700;
  font-size: 13px;
}

.chips {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 10px;
}

.chip {
  --h: 0;
  border: 2px solid rgba(0, 0, 0, 0.35);
  border-radius: 14px;
  min-height: 74px;
  display: grid;
  place-items: center;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  background: #eee;
}

.chip:nth-child(1) { --h: 0; }
.chip:nth-child(2) { --h: 60; }
.chip:nth-child(3) { --h: 120; }
.chip:nth-child(4) { --h: 180; }
.chip:nth-child(5) { --h: 240; }
.chip:nth-child(6) { --h: 300; }
  

HSL vs OKLCH “same lightness” vibe check

Click the snippets. Both attempt “same lightness across hues”. OKLCH typically looks more consistent.

HSL: hsl(H 90% 60%)
0
60
120
180
240
300
OKLCH: oklch(70% 0.17 H)
0
60
120
180
240
300

CSS OKLCH gradients

Gradients are where OKLCH often feels like a “cheat code”: the in-between colors tend to look smoother and less muddy.

Modern CSS also lets you influence how colors interpolate across a gradient. The interpolation color space for color blending defaults to Oklab, which is closely related to OKLCH.

Using in oklch for interpolation

You can explicitly request interpolation in OKLCH for a gradient using syntax like: linear-gradient(in oklch, ...). You can also tweak hue interpolation (for example, longer hue vs shorter hue) to control the path around the color wheel.

.hero {
  background: linear-gradient(
    90deg,
    oklch(70% 0.20 20),
    oklch(70% 0.20 310)
  );
}
  
.hero {
  background: linear-gradient(
    90deg in oklch,
    oklch(70% 0.20 20),
    oklch(70% 0.20 310)
  );
}
  
.hero {
  background: linear-gradient(
    90deg in oklch longer hue,
    oklch(70% 0.20 20),
    oklch(70% 0.20 310)
  );
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 980px;
  padding: 18px;
  display: grid;
  gap: 14px;
  font-family: system-ui, Arial, sans-serif;
}

.hero {
  border: 3px solid #111;
  border-radius: 18px;
  box-shadow: 0 12px 0 #111;
  min-height: 240px;
  padding: 16px;
  display: grid;
  align-content: end;
  overflow: hidden;
  position: relative;
  color: #fff;
}

.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("https://picsum.photos/1200/700") center / cover no-repeat;
  opacity: 0.25;
}

.hero > * {
  position: relative;
}

.hero h4 {
  margin: 0;
  font-size: 16px;
}

.hero p {
  margin: 0;
  line-height: 1.45;
  max-width: 68ch;
}

.note {
  margin-top: 10px;
  background: rgba(255, 255, 255, 0.78);
  border: 2px solid rgba(0, 0, 0, 0.35);
  border-radius: 14px;
  padding: 10px 12px;
  font-size: 13px;
  color: #333;
}
  

OKLCH gradient interpolation

Click the snippets: default interpolation vs in oklch vs in oklch longer hue. The hue path can noticeably change the “middle” colors.

Tip: If your mid-colors look weird, try changing the hue interpolation mode.

CSS OKLCH gradient animation

Animating gradients is usually tricky because gradients are images, and “image interpolation” can get messy. A reliable approach is to animate numbers (like hue) and build the gradient from those numbers. OKLCH shines here because hue rotations often look smooth while lightness stays stable.

Animating an OKLCH gradient with hue-rotate

Animating gradients can be surprisingly annoying because gradients are treated like images. A super reliable trick is to keep the gradient itself static, then animate a filter: hue-rotate() on the gradient layer. The result is a smooth “color spin” effect that works great for OKLCH gradients.

In the playground below, the gradient lives on a ::before pseudo-element (so it’s its own layer), and we rotate its hue from 0deg to 360deg forever. Try the second snippet as well: it uses animation-direction: alternate for a back-and-forth vibe.

.banner::before {
  animation: hue-spin 4s linear infinite;
}

@keyframes hue-spin {
to {
filter: hue-rotate(360deg);
}
} 
.banner::before {
  animation: hue-spin 4s linear infinite;
}

@keyframes hue-spin {
  to {
    filter: hue-rotate(360deg);
  }
}

.banner::before {
  animation-direction: alternate;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 980px;
  padding: 18px;
  font-family: system-ui, Arial, sans-serif;
}

.banner {
  border: 3px solid #111;
  border-radius: 18px;
  box-shadow: 0 12px 0 #111;
  min-height: 220px;
  padding: 18px;
  display: grid;
  align-content: end;
  position: relative;
  overflow: hidden;
  background: #fff;
}

.banner::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg in oklch,
    oklch(72% 0.20 20),
    oklch(72% 0.20 150),
    oklch(72% 0.20 280)
  );
}

.banner > * {
  position: relative;
}

.banner h4 {
  margin: 0;
  font-size: 16px;
}

.banner p {
  margin: 0;
  line-height: 1.45;
  max-width: 70ch;
}

.glass {
  margin-top: 10px;
  background: rgba(255, 255, 255, 0.8);
  border: 2px solid rgba(0, 0, 0, 0.35);
  border-radius: 14px;
  padding: 10px 12px;
  font-size: 13px;
}

.mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
  

Animation tip: keep the gradient on its own layer

Putting the gradient on ::before makes it easy to animate without affecting the text. Then we simply lift the content above it with position: relative. This pattern is also handy if you later want to add overlays, noise, or a background image under the text.

Learn more about gradients in the CSS Gradient Interactive Tutorial, and learn more about CSS animations in the CSS Animation Interactive Tutorial.

CSS OKLCH support and browser compatibility

OKLCH support is very good in modern browsers, but you should still plan for older ones. The easiest way to stay current is to check Can I use oklch().

Here’s the practical approach:

  • Provide a safe fallback color first (like #4f46e5 or hsl(...)).
  • Then override with OKLCH if supported using @supports.

Fallback pattern with @supports

This is the “friendly and boring” pattern that works well in real projects.

.badge {
  background: #4f46e5;
}

@supports (color: oklch(60% 0.15 260)) {
.badge {
background: oklch(60% 0.15 260);
}
} 
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 980px;
  padding: 18px;
  font-family: system-ui, Arial, sans-serif;
}

.badge {
  border: 3px solid #111;
  border-radius: 999px;
  box-shadow: 0 10px 0 #111;
  color: #fff;
  padding: 14px 16px;
  display: inline-flex;
  gap: 10px;
  align-items: center;
  font-weight: 700;
}

.dot {
  width: 12px;
  height: 12px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.85);
}

.note {
  margin-top: 12px;
  max-width: 72ch;
  line-height: 1.45;
}
  
OKLCH when supported

If OKLCH isn’t supported, you still get the fallback color. If it is supported, the OKLCH value wins.

Wrap-up

  • Use oklch(L C H / A) when you want predictable tweaks and smoother palettes.
  • Alpha works with the modern slash syntax: / 0.5 or / 50%.
  • Gradients often look nicer, and you can even request interpolation like in oklch.
  • For real-world safety, pair OKLCH with fallbacks via @supports.