Using CSS Color Spaces
A practical guide to CSS color spaces, covering RGB, HSL, Lab, LCH, OKLab, OKLCH, color(), color-mix(), gradients, conversions and choosing the right color model for modern web interfaces.
CSS started with a relatively small set of ways to describe colors: named colors, hexadecimal notation and RGB. HSL later became popular because its channels are easier for humans to reason about. Modern CSS goes much further. The Color 4 specifications introduced support for perceptual color spaces such as Lab, LCH, OKLab and OKLCH, as well as wider-gamut color spaces through the color() function.
These newer color spaces are especially useful when building design systems, generating palettes, creating smooth gradients, adjusting lightness, and working with displays that support colors beyond the traditional sRGB gamut. You do not need to replace every HEX or RGB value in an existing project, but understanding the available color spaces helps you choose a representation that matches the job.
What Is a CSS Color Space?
A color space is a mathematical model used to represent colors. It defines which dimensions or channels describe a color and how numerical values in those channels correspond to visible colors. HEX, RGB, HSL, Lab and OKLCH are different ways of representing color information rather than completely different types of colors.
For example, the same visual color can be represented as a hexadecimal RGB value, an rgb() color, an HSL color, or a color expressed in a perceptual space such as OKLCH. The representation changes, while the intended color can remain approximately the same.
| Color space or model | Main idea | Typical use |
|---|---|---|
| HEX / sRGB | Red, green and blue channels | Static UI colors and compatibility |
| RGB | Numeric red, green and blue channels | UI colors and alpha control |
| HSL | Hue, saturation and lightness | Simple color manipulation |
| HWB | Hue, whiteness and blackness | Human-friendly color adjustments |
| Lab | Perceptual lightness and opponent color axes | Perceptual color manipulation |
| LCH | Lightness, chroma and hue | Palette and color adjustments |
| OKLab | Modern perceptual color space | Consistent lightness and mixing |
| OKLCH | OKLab expressed as lightness, chroma and hue | Modern UI palettes and gradients |
| color() | Explicit color space and components | Wide-gamut colors |
sRGB: The Traditional Foundation
Most CSS colors encountered in everyday web development belong to the sRGB color space. Traditional hexadecimal notation such as #3366cc represents an sRGB color using red, green and blue components encoded as hexadecimal values.
.button {
background: #3366cc;
color: #ffffff;
}
.card {
background: rgb(255 255 255);
}
.overlay {
background: rgb(0 0 0 / 50%);
}The rgb() function is especially useful when you want to make the individual channels or alpha value explicit. Modern CSS also allows the space-separated rgb() syntax, which is generally easier to combine with newer CSS color features.
HEX Colors
HEX is one of the most compact and familiar ways to write sRGB colors. A six-digit value contains two hexadecimal digits for red, green and blue. CSS also supports three-digit shorthand and eight-digit notation when an alpha channel is included.
.primary {
color: #3366cc;
}
.primary-short {
color: #36c;
}
.primary-alpha {
color: #3366cc80;
}HEX is excellent for fixed design tokens and is still one of the easiest formats to read and copy between design tools and development tools. Its main limitation is that the numerical channels do not correspond to concepts such as hue or perceptual lightness.
RGB and Alpha
RGB represents a color using red, green and blue components. CSS also allows an alpha component after a slash, making transparency explicit without requiring a separate rgba() function.
.surface {
background: rgb(245 247 250);
}
.overlay {
background: rgb(20 25 35 / 0.75);
}
.border {
border-color: rgb(51 102 204 / 50%);
}RGB is useful when you already have channel values from an image, API or design specification. It is less convenient when your goal is to make a color lighter, darker or more saturated in a predictable way.
HSL: Hue, Saturation and Lightness
HSL separates color into hue, saturation and lightness. Hue is represented as an angle around a color wheel, while saturation and lightness describe the other two dimensions.
.primary {
color: hsl(220 70% 50%);
}
.primary-transparent {
color: hsl(220 70% 50% / 0.7);
}
.background {
background: hsl(220 30% 96%);
}HSL is convenient for many common design tasks. For example, changing the lightness value can produce a darker or lighter variation without manually calculating three RGB channels.
.primary {
color: hsl(220 70% 50%);
}
.primary-dark {
color: hsl(220 70% 35%);
}
.primary-light {
color: hsl(220 70% 70%);
}HWB: Hue, Whiteness and Blackness
HWB is another CSS color model built around hue. Instead of saturation and lightness, it uses whiteness and blackness. This can be useful when you want to describe a color as a base hue mixed with white or black.
.soft-blue {
color: hwb(220 20% 10%);
}
.dark-blue {
color: hwb(220 5% 40%);
}HWB is relatively easy to understand, but modern design systems often reach for OKLCH when perceptual adjustments and predictable lightness are important.
Why Modern CSS Needs More Color Spaces
Traditional RGB-based color models were designed around a particular color gamut and display technology. Modern screens can reproduce colors outside the sRGB gamut, and modern CSS needs a way to describe those colors.
There is another issue as well: numerical changes in RGB or HSL do not always correspond to visually uniform changes. Increasing a channel by the same amount does not necessarily make two colors appear to change by the same perceptual amount.
Perceptual color spaces such as Lab and OKLab attempt to organize colors so that numerical distances correspond more closely to perceived differences. This makes them particularly useful for palette generation, gradients and systematic color manipulation.
Lab Color Space
CSS supports the Lab color function for expressing colors using three components: lightness, the a axis and the b axis. Lightness is represented by L, while a and b describe opposing color dimensions.
.brand {
color: lab(55% 40 20);
}
.soft-brand {
color: lab(75% 25 10);
}The important difference from RGB and HSL is that Lab is designed around perceptual relationships. This makes it more suitable when you need to manipulate perceived lightness or color distance rather than raw display channels.
LCH: Lightness, Chroma and Hue
LCH is a cylindrical representation related to Lab. Instead of the a and b axes, it uses lightness, chroma and hue. This makes the model easier to reason about when designing color palettes.
.brand {
color: lch(60% 70 250);
}
.brand-light {
color: lch(75% 55 250);
}
.brand-dark {
color: lch(45% 55 250);
}In LCH, lightness controls the perceptual lightness dimension, chroma represents colorfulness, and hue is an angle. This resembles HSL conceptually, but the underlying color model is different.
OKLab
OKLab is a newer perceptual color space designed to provide useful perceptual uniformity while being practical for computer graphics and color manipulation. CSS exposes it through the oklab() function.
.accent {
color: oklab(65% 0.1 0.12);
}
.accent-soft {
color: oklab(80% 0.06 0.08);
}OKLab is particularly useful when you want to adjust lightness while keeping the general character of a color stable. It is also important because OKLab provides the foundation for OKLCH.
OKLCH
OKLCH is a cylindrical representation of OKLab. Its three main components are lightness, chroma and hue. For many modern CSS workflows, OKLCH is one of the easiest advanced color spaces to work with because its parameters map reasonably well to common design concepts.
.primary {
color: oklch(60% 0.18 250);
}
.primary-light {
color: oklch(75% 0.14 250);
}
.primary-dark {
color: oklch(45% 0.16 250);
}The first value is lightness, the second is chroma and the third is hue. Keeping chroma and hue relatively stable while changing lightness can produce useful light and dark variants.
Understanding OKLCH Channels
OKLCH becomes much easier to use once its three channels are understood. Lightness is expressed as a percentage. Chroma represents colorfulness and is commonly written as a decimal value. Hue is represented as an angle.
| Channel | Meaning | Example |
|---|---|---|
| L | Perceptual lightness | 60% |
| C | Chroma or colorfulness | 0.18 |
| H | Hue angle | 250 |
A value such as oklch(60% 0.18 250) therefore describes a color with 60% lightness, relatively high chroma and a hue angle of 250 degrees.
Comparing HSL and OKLCH
HSL and OKLCH look similar because both expose a hue-like channel alongside lightness and a measure of colorfulness. They should not be treated as interchangeable, however. Their underlying mathematics and perceptual behavior are different.
| Feature | HSL | OKLCH |
|---|---|---|
| Hue | Angle | Angle |
| Lightness | HSL lightness | Perceptual lightness |
| Colorfulness | Saturation | Chroma |
| Perceptual uniformity | Limited | Better suited |
| Palette generation | Useful | Often more predictable |
| Modern wide-gamut workflows | Limited | Very useful |
For simple UI work, HSL remains perfectly useful. For systematic palette generation, smooth color interpolation and modern color workflows, OKLCH is often a better starting point.
Using the color() Function
The color() function allows CSS to specify a color space explicitly. This is especially important for color spaces that cannot be represented using traditional hexadecimal or RGB notation.
.wide-gamut {
color: color(display-p3 1 0.2 0.1);
}
.custom-color-space {
color: color(srgb 0.2 0.4 0.8);
}The first argument identifies the color space, followed by the corresponding component values. One important use is display-p3, which can represent colors outside the sRGB gamut on supported displays.
sRGB and Display-P3
sRGB is still the common baseline for the web, but modern displays can support wider gamuts. Display-P3 provides access to a larger range of colors, particularly in highly saturated areas.
.accent {
color: color(display-p3 0.9 0.1 0.2);
}Wide-gamut colors should be introduced deliberately. If a browser or display cannot reproduce a particular color, the browser must map it into a supported gamut. A design should therefore still behave sensibly when wide-gamut colors are unavailable.
Color Gamut Explained
A color gamut is the range of colors that a particular color space or device can represent. Two color spaces can describe different ranges even when both use three numerical components.
This is why a color value cannot always be copied directly from one color space to another. A color that is valid and highly saturated in a wide-gamut space may fall outside the sRGB gamut.
Color Mixing with color-mix()
The color-mix() function allows CSS to mix two colors in a selected interpolation color space. This is useful for creating states, variants and gradients without manually calculating intermediate colors.
.mixed {
color: color-mix(in srgb, #3366cc, white);
}
.mixed-oklch {
color: color-mix(in oklch, #3366cc, white);
}
.mixed-p3 {
color: color-mix(in display-p3, red 30%, blue);
}The choice of interpolation space matters. Mixing in sRGB and mixing in OKLCH can produce visibly different intermediate colors. For perceptual color transitions, OKLCH is often worth considering.
Why the Mixing Color Space Matters
Suppose you want to create a gradient or transition between two colors. If the interpolation happens directly through RGB coordinates, the intermediate colors follow a path through that RGB space. A perceptual space can produce a different path that may look smoother to human observers.
.rgb-mix {
background: color-mix(in srgb, #ff0000, #0000ff);
}
.oklch-mix {
background: color-mix(in oklch, #ff0000, #0000ff);
}There is no universal rule that one interpolation space is always correct. The appropriate choice depends on the visual result you want. OKLCH is particularly useful when the goal is a visually coherent transition rather than simply interpolating RGB channel values.
Color Spaces and CSS Gradients
Modern CSS gradients can specify an interpolation color space, giving developers more control over how colors transition between stops.
.gradient {
background: linear-gradient(
90deg in oklch,
#4f46e5,
#ec4899
);
}This can be particularly useful for colorful gradients where interpolation in a traditional RGB space produces unwanted intermediate colors or a visually uneven transition.
Color Hues and Interpolation
Cylindrical color spaces such as HSL, LCH and OKLCH represent hue as an angle. When a browser interpolates colors between different hues, it must determine how to travel around the hue circle.
CSS provides hue interpolation methods such as shorter, longer, increasing and decreasing. These options can matter when two colors are on opposite sides of the hue wheel.
.gradient {
background: linear-gradient(
90deg in oklch shorter hue,
oklch(65% 0.2 20),
oklch(65% 0.2 340)
);
}Without understanding hue interpolation, a gradient can travel around the color wheel in a direction that is not what you expected.
Creating Light and Dark Variants
One of the most useful applications of perceptual color spaces is generating related color variants. Instead of manually choosing unrelated HEX values, you can adjust a lightness dimension while keeping the other properties relatively stable.
:root {
--brand-50: oklch(97% 0.02 250);
--brand-100: oklch(93% 0.04 250);
--brand-300: oklch(82% 0.10 250);
--brand-500: oklch(62% 0.18 250);
--brand-700: oklch(48% 0.15 250);
--brand-900: oklch(32% 0.10 250);
}This approach is useful for design tokens because the scale has an explicit relationship between the colors. However, you should still validate the final colors visually and for contrast rather than assuming that equal numerical changes always produce a perfect palette.
Using CSS Custom Properties
CSS custom properties work with all modern CSS color functions, making color spaces particularly useful for design systems.
:root {
--color-primary: oklch(62% 0.18 250);
--color-primary-hover: oklch(55% 0.18 250);
--color-surface: oklch(98% 0.01 250);
--color-text: oklch(25% 0.02 250);
}
.button {
background: var(--color-primary);
color: white;
}
.button:hover {
background: var(--color-primary-hover);
}Keeping the color definitions in custom properties makes it easier to maintain themes and modify an entire palette without searching through individual components.
Building Light and Dark Themes
Color spaces can also help organize light and dark themes. A theme should not necessarily be created by simply inverting colors. Surface, text, border and accent colors often need different relationships.
:root {
--surface: oklch(98% 0.01 250);
--surface-raised: oklch(95% 0.015 250);
--text: oklch(25% 0.02 250);
--muted: oklch(52% 0.025 250);
--primary: oklch(58% 0.18 250);
}
[data-theme="dark"] {
--surface: oklch(20% 0.02 250);
--surface-raised: oklch(24% 0.025 250);
--text: oklch(94% 0.015 250);
--muted: oklch(72% 0.02 250);
--primary: oklch(72% 0.16 250);
}The advantage is not that OKLCH automatically creates a perfect dark theme. Rather, it gives you a useful model for deliberately controlling lightness and colorfulness while designing the relationships between tokens.
Color Spaces and Accessibility
Color space choice does not replace accessibility testing. Text and background combinations still need sufficient contrast, and visual distinctions should not rely on color alone when the information is important.
Perceptual spaces can make palette construction easier, but accessibility should be checked using an appropriate contrast calculation rather than inferred from the L, C or H values of a color.
.status {
color: oklch(50% 0.16 25);
}
.status-icon {
margin-inline-end: 0.5rem;
}
.status-text {
font-weight: 600;
}For example, an error state should not communicate its meaning through red text alone. An icon, label, pattern or other semantic indicator can provide additional information for users who cannot distinguish the colors.
Relative Color Syntax
Modern CSS also supports relative color syntax. It allows a new color to be derived from another color while accessing its channels. This can make theme and component variations more dynamic.
:root {
--primary: oklch(62% 0.18 250);
}
.button {
background: var(--primary);
}
.button:hover {
background: oklch(
from var(--primary)
calc(l - 0.08)
c
h
);
}Relative color syntax is especially interesting for component libraries because a component can derive a state color from a base token instead of requiring a separate hard-coded color for every state.
Fallbacks for Modern Color Functions
A common progressive-enhancement pattern is to provide a conventional color first and then override it with a modern color function.
.button {
background: #3366cc;
color: #fff;
}
@supports (color: oklch(60% 0.15 250)) {
.button {
background: oklch(60% 0.15 250);
}
}This pattern allows older environments to keep using the fallback while capable browsers use the newer color representation.
Out-of-Gamut Colors
A color can be mathematically valid in one color space while being outside the gamut of another. This is particularly important with OKLCH because not every combination of lightness, chroma and hue can be represented by sRGB.
For example, increasing chroma while keeping lightness and hue fixed can eventually produce a color that cannot be represented by an sRGB display. Browsers perform gamut mapping when necessary, which means the rendered color may differ from the theoretical input.
.accent {
color: oklch(65% 0.35 30);
}This is one reason to avoid assuming that increasing chroma always makes a visible color look proportionally more saturated. Once the color approaches or exceeds the target gamut, the final result is constrained by what can actually be displayed.
Color Conversion Is Not Just Text Replacement
Converting #3366cc to another notation is not equivalent to simply changing the syntax. A real conversion must interpret the source color in its color space and calculate the corresponding coordinates in the destination space.
This distinction becomes especially important when converting between sRGB and perceptual or wide-gamut spaces. The resulting numerical values can look completely different even though they describe the same intended color.
When to Use HEX
- You need a compact representation for static sRGB colors.
- You are defining simple design tokens.
- You want a format that is familiar across CSS, design tools and documentation.
- You do not need perceptual color manipulation.
- You need straightforward compatibility with existing tooling.
When to Use RGB
- You already have RGB channel values.
- You are working with image or graphics data.
- You need explicit control over red, green and blue channels.
- You frequently work with alpha values.
- You are integrating values from an API or another RGB-based system.
When to Use HSL
- You want a familiar hue-based representation.
- You need simple manual lightness or saturation adjustments.
- Your existing design system already uses HSL.
- You are building straightforward color controls.
- Perceptual uniformity is not a major requirement.
When to Use OKLCH
- You are building a modern design system.
- You need systematic light and dark color variants.
- You want more predictable perceptual lightness adjustments.
- You are creating sophisticated gradients or color transitions.
- You want a convenient representation for modern wide-gamut workflows.
- You are generating palettes programmatically.
When to Use Lab or LCH
- You need a perceptual color representation.
- You are working with color interpolation or analysis.
- You have existing Lab/LCH color data.
- You need to integrate with workflows that already use these spaces.
A Practical Color Space Strategy
There is no requirement to choose one color space for an entire project. Different representations can coexist. A design system may store modern tokens in OKLCH while exporting sRGB fallbacks for compatibility. Components can consume custom properties without caring which color space produced them.
A practical approach is to use the simplest representation that solves the problem. HEX is still excellent for fixed colors. HSL is useful for basic human-readable manipulation. OKLCH becomes increasingly valuable when you need systematic palette generation, perceptual adjustments or advanced gradients.
| Task | Good starting point |
|---|---|
| Simple static color | HEX |
| RGB channel manipulation | RGB |
| Simple hue-based editing | HSL |
| White/black adjustments | HWB |
| Perceptual color manipulation | Lab or OKLab |
| Perceptual hue-based palettes | LCH or OKLCH |
| Wide-gamut color | color() |
| Modern color mixing | color-mix() |
| Modern gradients | OKLCH interpolation |
| Design-system tokens | OKLCH with fallbacks where needed |
Using Color Spaces in JavaScript
Modern web applications often need to manipulate colors dynamically. For example, a theme editor may allow users to modify a color and immediately generate CSS variables. The application can represent the color in a perceptual space while eventually serializing it as CSS.
const primary = {
lightness: 0.62,
chroma: 0.18,
hue: 250,
};
const cssColor =
`oklch(${primary.lightness * 100}% ${primary.chroma} ${primary.hue})`;
console.log(cssColor);For serious color conversion work, use a color library that understands the relevant color spaces and gamut mapping rules rather than implementing conversions from scratch unless you specifically need to work with the underlying mathematics.
Color Spaces in Design Tokens
Design tokens are a natural place to use modern color spaces. Instead of scattering literal colors throughout a stylesheet, define semantic variables such as primary, surface, text and border colors.
:root {
--color-primary: oklch(62% 0.18 250);
--color-primary-hover: oklch(55% 0.18 250);
--color-primary-muted: oklch(88% 0.06 250);
--color-surface: oklch(98% 0.01 250);
--color-border: oklch(88% 0.02 250);
--color-text: oklch(25% 0.02 250);
--color-text-muted: oklch(55% 0.02 250);
}Semantic naming is more important than the particular color space. A component should generally ask for a semantic token such as --color-primary rather than depending directly on a raw OKLCH value.
Common Mistakes
Treating HSL Lightness as Perceptual Brightness
HSL lightness is useful but is not a reliable perceptual brightness scale. Two HSL colors with identical lightness values can appear quite different to the eye.
Assuming Every Color Space Has the Same Gamut
Color spaces describe different ranges and coordinates. A value that works in one space may fall outside the gamut of another. Always consider the target display and browser behavior.
Mixing Colors Without Considering the Interpolation Space
Two colors can produce different intermediate colors depending on the space used for interpolation. This matters for gradients, animations and generated palettes.
Replacing Every HEX Value with OKLCH
Modern does not automatically mean better for every situation. If a simple static color is all you need, HEX remains clear and practical. Use advanced color spaces when they provide a concrete benefit.
Ignoring Contrast After Generating a Palette
A mathematically consistent palette can still contain combinations that are difficult to read. Always validate important foreground and background pairs independently.
Hard-Coding Too Many Color Variants
If every component contains its own collection of slightly different colors, maintaining a theme becomes difficult. Prefer semantic custom properties and generate or define a controlled palette.
Browser Support and Progressive Enhancement
Modern CSS color functions have strong support in current browsers, but projects may still need to account for older browsers, embedded webviews or specific supported environments. Check browser compatibility before making advanced color syntax a hard requirement.
.card {
background: #ffffff;
}
@supports (background: oklch(98% 0.01 250)) {
.card {
background: oklch(98% 0.01 250);
}
}Progressive enhancement is often enough for UI colors. A fallback can provide a sensible sRGB representation while modern browsers receive the more precise or wider-gamut color.
Debugging CSS Color Problems
- Check whether the color function is supported by the target browser.
- Inspect the computed color in browser developer tools.
- Check whether a color is outside the target gamut.
- Compare the same color in different color spaces rather than comparing raw numbers.
- Check the interpolation space when debugging gradients or transitions.
- Test foreground and background colors together rather than evaluating them independently.
- Provide an sRGB fallback when compatibility is important.
Frequently Asked Questions
What is the best CSS color space?
There is no single best color space for every task. HEX and RGB are excellent for conventional sRGB colors, HSL is convenient for simple hue-based manipulation, and OKLCH is particularly useful for modern perceptual palette work, gradients and systematic color adjustments.
Should I use OKLCH instead of HEX?
Not necessarily. HEX remains a simple and effective format for fixed sRGB colors. OKLCH becomes useful when you need perceptual manipulation, generated palettes, modern gradients or more explicit control over lightness, chroma and hue.
Is OKLCH better than HSL?
OKLCH is generally better suited to perceptual color manipulation, while HSL remains convenient for simple human-readable color adjustments. They solve similar-looking problems using different underlying color models.
What is the difference between Lab and OKLab?
Both are perceptual color spaces, but OKLab was designed as a newer practical alternative with useful perceptual properties for modern graphics and color manipulation. CSS supports both through lab() and oklab().
What is LCH compared with OKLCH?
LCH is a cylindrical representation related to Lab, while OKLCH is the cylindrical representation of OKLab. Both expose lightness, chroma and hue, but their underlying color spaces are different.
Can I use OKLCH in CSS variables?
Yes. OKLCH values can be stored directly in CSS custom properties and reused throughout a design system. This makes it practical for defining semantic colors and theme tokens.
Why do the same colors look different when mixed in RGB and OKLCH?
The browser calculates the intermediate colors in the selected interpolation space. RGB and OKLCH describe colors differently, so the path between two endpoints can produce different intermediate colors.
Helpful Color Tools
A color converter is useful when you need to move between HEX, RGB, HSL and other representations while working on a design system. Color pickers are convenient for inspecting and selecting individual colors, especially when starting from a visual reference.
For gradients, a CSS gradient generator can help experiment with color stops, directions and interpolation approaches. Combining these tools with browser developer tools makes it easier to compare traditional sRGB colors with modern perceptual color spaces.
Conclusion
CSS color spaces have evolved far beyond HEX and RGB. HSL and HWB provide convenient human-oriented models, while Lab, LCH, OKLab and OKLCH provide more perceptually useful representations for advanced color manipulation. The color() function additionally makes wide-gamut spaces available when modern displays can take advantage of them.
For everyday static colors, there is no reason to abandon HEX or RGB. HSL remains useful for straightforward color adjustments. When building a modern design system, generating palettes, creating sophisticated gradients or manipulating colors based on perceptual properties, OKLCH is often a particularly useful choice.
The most important skill is not memorizing every color function. It is understanding what each color space represents and choosing the representation that matches the task. Once that distinction is clear, modern CSS color features become practical tools rather than a collection of unfamiliar syntaxes.