OKLCH color-mix examples showing UI tints, borders, hover states, and CSS tokens.

Tutorial

CSS color-mix() with OKLCH: Build Practical UI Color Ramps

Learn how to use CSS color-mix() with OKLCH to create hover colors, soft surfaces, borders, and UI color ramps with safe fallbacks.

Publishedcss color

Quick answer

CSS color-mix() with OKLCH mixes two colors in a perceptual color space to derive related UI tints, surfaces, borders, and states. Treat mixed colors as candidates, keep fallbacks where needed, and verify computed output and contrast before shipping.

This tutorial shows how to use CSS color-mix() with OKLCH to create practical UI colors from one base color. By the end, you will have a small token set for hover states, soft surfaces, borders, and focus outlines, plus fallback declarations and an @supports pattern for environments where you do not want to rely only on modern color syntax.

The goal is not to use a new CSS feature just because it exists. The goal is to make related UI colors easier to maintain, review, and test. OKLCH is useful because it organizes color around perceptual lightness, chroma, and hue, which often makes ramps feel more even than quick mixes in sRGB or HSL.

This workflow is especially helpful for systems that need repeated color decisions: hover states, selected states, alert surfaces, quiet borders, and preview components that should feel related without hand-picking every single value.

The CSS snippets below are starting patterns, not drop-in production tokens. Treat the generated values as candidates until you have checked computed output, contrast, browser support targets, and real component context.

Prerequisites

  • A base brand or accent color you already trust.
  • A stylesheet where CSS custom properties are allowed.
  • A fallback color for each important token.
  • A contrast checker for any text, icon, or UI state that must remain readable.

If you need a quick refresher on modern color syntax, the CSS color formats guide covers HEX, RGB, HSL, OKLCH, color-mix(), alpha, and fallback context before you start wiring tokens into a UI.

Why Mix in OKLCH

color-mix() asks for an interpolation space. That space decides how the browser travels from one color to another. Mixing in srgb can be fine for small utility effects, but RGB channels are not organized around human perception. OKLCH gives you a more design-friendly way to think about the result: lightness, chroma, and hue.

That does not mean OKLCH magically fixes every palette. It means the intermediate colors are often easier to reason about when you care about perceived lightness and colorful intensity. You still need to check contrast, gamut, and final appearance in the real component.

How color-mix() Percentages Work

In a color-mix() expression, percentages describe how much of each color participates in the mix. If you provide one percentage, the remaining share is assigned to the other color. For example, mixing a brand color at 20% with white creates a very pale tint. Mixing the brand color at 85% with black keeps the brand dominant while darkening it.

mix-percentages.cssThe first percentage controls how much of the brand color remains in the mix.
:root {
  --brand-tint: color-mix(in oklch, var(--brand-oklch) 20%, white);
  --brand-hover: color-mix(in oklch, var(--brand-oklch) 85%, black);
  --brand-muted: color-mix(in oklch, var(--brand-oklch) 55%, gray);
}

Step 1: Start with a Stable Base Color

Start with one approved color and store it as a custom property. The fallback can be a HEX value, while the modern value can be OKLCH. Keeping both close together makes the intent easier to review.

tokens.cssUse a stable fallback value and an OKLCH value for modern color operations.
:root {
  --brand: #2f6fed;
  --brand-oklch: oklch(0.58 0.21 263);
}

Step 2: Mix Toward White for Soft Surfaces

A common use for color-mix() is creating a soft background from an accent color. In OKLCH, mixing toward white can create a lighter surface that keeps more of the original hue direction.

tokens.cssKeep fallback token values outside @supports and modern color-mix() values inside the support check.
:root {
  --brand-surface: #eef4ff;
  --brand-surface-strong: #d9e6ff;
}

@supports (background-color: color-mix(in oklch, black 50%, white)) {
  :root {
    --brand-surface: color-mix(in oklch, var(--brand-oklch) 12%, white);
    --brand-surface-strong: color-mix(in oklch, var(--brand-oklch) 24%, white);
  }
}

Step 3: Mix Toward Black for Hover and Active States

For button hover and active states, mix the base color toward black in small steps. Keep the movement modest. Large jumps can make the UI feel inconsistent and may change contrast more than expected.

buttons.cssUse background-color for color-only changes, keep fallback states outside @supports, and verify final contrast in the actual UI.
.button-primary {
  background-color: var(--brand);
  color: white;
}

.button-primary:hover {
  background-color: #275ed0;
}

.button-primary:active {
  background-color: #214fae;
}

@supports (background-color: color-mix(in oklch, black 50%, white)) {
  .button-primary {
    background-color: var(--brand-oklch);
  }

  .button-primary:hover {
    background-color: color-mix(in oklch, var(--brand-oklch) 88%, black);
  }

  .button-primary:active {
    background-color: color-mix(in oklch, var(--brand-oklch) 78%, black);
  }
}

Step 4: Create Borders and Focus Rings

Borders and focus rings need enough contrast to be perceived against adjacent surfaces. A useful pattern is to mix the base color toward white for subtle borders and use a stronger, less diluted mix for focus outlines.

components.cssFocus styles need to be visible in context, not merely related to the brand color.
.field {
  border: 1px solid #b8c9ef;
}

.field:focus-visible {
  outline: 3px solid #5f8df5;
  outline-offset: 3px;
}

@supports (outline-color: color-mix(in oklch, black 50%, white)) {
  .field {
    border-color: color-mix(in oklch, var(--brand-oklch) 35%, white);
  }

  .field:focus-visible {
    outline-color: color-mix(in oklch, var(--brand-oklch) 76%, white);
  }
}

Step 5: Keep Fallbacks Close to the Modern Value

A fallback should be a real color, not a placeholder. For simple properties, a fallback declaration followed by a modern declaration can work well. For custom-property token systems, an @supports block is clearer because unsupported modern color functions inside variables can be harder to reason about than a plain ignored declaration.

fallback-pattern.cssThe fallback pattern is explicit: stable values first, modern values only inside the feature query.
.notice {
  background-color: #eef4ff;
  border-color: #b8c9ef;
  color: #102a36;
}

@supports (background-color: color-mix(in oklch, black 50%, white)) {
  .notice {
    background-color: color-mix(in oklch, var(--brand-oklch) 12%, white);
    border-color: color-mix(in oklch, var(--brand-oklch) 35%, white);
  }
}
Workflow diagram showing a source color, fallback tokens, OKLCH color-mix enhancement, and component review before shipping CSS.
Use color-mix() as part of a token workflow: write fallbacks first, enhance inside @supports, then verify computed UI pairs before freezing roles.

Step 6: Verify the Result

After generating colors, test the actual pairs. color-mix() can make a palette easier to maintain, but it does not guarantee accessible contrast, brand fit, or good dark-mode behavior. Use the CSS color generator to inspect generated values, then check important foreground and background pairs before shipping.

  1. Inspect the computed color in browser dev tools.
  2. Check text contrast on every mixed background.
  3. Check focus rings against both the component and surrounding surface.
  4. Test hover and active states with keyboard and pointer interaction.
  5. Keep documented fallbacks and @supports coverage for production-critical colors.

Step 7: Freeze Important Results as Tokens

Generated colors are convenient, but important product colors should still become named tokens once they are approved. That gives designers, developers, and reviewers a stable contract. The color can be produced with color-mix() during exploration and then documented as a named role such as --color-action-primary-hover or --color-info-surface.

This is especially useful when a mixed value affects accessibility, brand review, screenshots, or exported design documentation. A formula explains where the color came from, but a token explains how the color should be used.

Example Token Set

A practical OKLCH color-mix token set from one brand color.
TokenPurposeExample formulaReview note
--color-brand-surfaceSoft backgroundcolor-mix(in oklch, var(--brand-oklch) 12%, white)Check text placed on top.
--color-brand-surface-strongStronger tintcolor-mix(in oklch, var(--brand-oklch) 24%, white)Useful for selected or emphasized surfaces.
--color-brand-borderSubtle bordercolor-mix(in oklch, var(--brand-oklch) 35%, white)Verify it is visible against the surrounding surface.
--color-brand-focusFocus outlinecolor-mix(in oklch, var(--brand-oklch) 76%, white)Check against both the component and page background.
--color-brand-hoverButton hovercolor-mix(in oklch, var(--brand-oklch) 88%, black)Check label contrast and state distinction.
--color-brand-activeButton activecolor-mix(in oklch, var(--brand-oklch) 78%, black)Avoid jumps that feel disconnected from the base color.

Common Mistakes

  • Mixing every state by the same percentage without checking contrast.
  • Assuming OKLCH hue numbers match HSL hue numbers exactly.
  • Using color-mix() output for text without testing the final foreground/background pair.
  • Removing fallbacks before confirming the site's support target.
  • Putting modern color functions inside custom properties without a feature-query plan.
  • Treating generated ramps as final design tokens without review.
  • Using the background shorthand when only background-color should change.

For deeper implementation details around interpolation and color-space choice, Hue Codex documents its approach in the color mixing and gradients methodology.

FAQ

Why use OKLCH with color-mix()?

OKLCH organizes color around lightness, chroma, and hue, which can make generated UI ramps easier to reason about than channel-based mixes in sRGB.

Does color-mix() guarantee accessible colors?

No. color-mix() can generate related colors, but every important foreground and background pair still needs to be checked for contrast and real component context.

Should I keep fallback colors?

Yes, if your production support target includes browsers, renderers, email clients, screenshots, or tooling where modern CSS color support is uncertain. Use stable fallback values and consider @supports when custom properties contain modern color functions.

Should generated colors become design tokens?

Yes for important UI roles. A formula explains how a color was produced, but a named token explains how the color should be used.

Final Takeaway

CSS color-mix() with OKLCH is strongest when it supports a clear token workflow: start with an approved base color, generate related surfaces and states, keep fallbacks nearby, and verify the actual UI pairs. As a next step, generate one surface, one hover state, one border, and one focus outline, then freeze only the values that pass contrast, support, and component review.

Spot an issue or want a color topic covered?Send a correction or request a post.