Modern CSS Color Functions: rgb(), hsl(), oklch(), and More

May 26, 202612 min read

The Evolution of CSS Color Syntax

CSS color handling has undergone a remarkable transformation in recent years. Gone are the days when hex codes and named colors were our only options. Today's CSS provides a rich set of color functions that offer greater precision, better readability, and more powerful color manipulation capabilities.

The evolution has moved from the limited color: red; and #FF0000 syntax toward modern functions that separate hue, saturation, and lightness in intuitive ways. This progression isn't just about adding features—it's about making color in CSS more predictable, accessible, and powerful.

Legacy Syntax: hex and rgb()

Hexadecimal Colors

Hex codes have been the backbone of web colors for decades:

.hero {
  background-color: #3B82F6; /* Blue */
  color: #FFFFFF; /* White */
}

Hex notation represents colors using three pairs of hexadecimal digits (00-FF) for red, green, and blue. While compact, hex has significant drawbacks:

  • Difficult to intuitively understand what color #E879F0 represents
  • No built-in way to adjust lightness or saturation
  • No alpha/transparency support without the #RRGGBBAA extension (which has poor browser support)

The Old rgb() Function

The traditional rgb() function required comma-separated values:

.button {
  background-color: rgb(59, 130, 246); /* Blue */
  color: rgb(255, 255, 255); /* White */
}

With alpha transparency, you'd use rgba():

.overlay {
  background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black */
}

Modern rgb() and rgba() Syntax

CSS Color Level 4 introduced a space-separated syntax that's more consistent with other color functions:

.modern-rgb {
  background-color: rgb(59 130 246); /* No commas! */
  color: rgb(255 255 255 / 0.9); /* Alpha with slash syntax */
}

.modern-rgba {
  background-color: rgba(59 130 246 / 50%); /* Percentage alpha */
}

Key Improvements

  1. Space separation: rgb(59 130 246) instead of rgb(59, 130, 246)
  2. Slash for alpha: rgb(255 255 255 / 0.5) instead of needing rgba()
  3. Percentage values: rgb(50% 50% 100%) is now valid
  4. None keyword: rgb(none none none) for intentionally missing components
/* All these are now valid */
.valid-examples {
  color: rgb(255 0 0);
  color: rgb(100% 0% 0%);
  color: rgb(255 0 0 / 0.5);
  color: rgb(255 0 0 / 50%);
  color: rgb(none none none); /* Resets to default */
}

hsl(): Hue, Saturation, Lightness

The hsl() function represents colors using a cylindrical coordinate system that aligns better with human perception:

.hsl-example {
  /* hsl(hue saturation lightness) */
  background-color: hsl(217 91% 60%); /* Same blue as before */
}

Understanding HSL Components

  • Hue (0-360): The color type, measured in degrees on the color wheel
    • 0° = Red, 120° = Green, 240° = Blue
  • Saturation (0-100%): Color intensity (0% = gray, 100% = vivid)
  • Lightness (0-100%): Brightness (0% = black, 50% = normal, 100% = white)
.color-palette {
  --primary-hue: 217;
  
  --primary-50: hsl(var(--primary-hue) 91% 95%);
  --primary-100: hsl(var(--primary-hue) 91% 90%);
  --primary-200: hsl(var(--primary-hue) 91% 80%);
  --primary-300: hsl(var(--primary-hue) 91% 70%);
  --primary-400: hsl(var(--primary-hue) 91% 60%);
  --primary-500: hsl(var(--primary-hue) 91% 50%);
  --primary-600: hsl(var(--primary-hue) 91% 40%);
  --primary-700: hsl(var(--primary-hue) 91% 30%);
  --primary-800: hsl(var(--primary-hue) 91% 20%);
  --primary-900: hsl(var(--primary-hue) 91% 10%);
}

Modern hsl() Syntax

Like rgb(), modern hsl() uses space separation:

.modern-hsl {
  color: hsl(217 91% 60%);
  color: hsl(217 91% 60% / 0.8); /* With alpha */
  color: hsl(none none none); /* Reset */
}

oklch(): The Perceptually Uniform Revolution

oklch() is the newest and most scientifically advanced CSS color function. It uses the Oklab color space, which is perceptually uniform—meaning equal numerical changes produce equal perceived color differences.

.oklch-example {
  /* oklch(lightness chroma hue) */
  background-color: oklch(60% 0.2 264); /* Perceptually pleasing blue */
}

Components of oklch()

  • Lightness (0-100%): Perceptually uniform lightness (0% = black, 50% = middle gray, 100% = white)
  • Chroma (0-0.4+): Colorfulness/saturation (0 = gray, higher = more vivid)
  • Hue (0-360): Same as hsl()—the color wheel position

Why oklch() is Superior

  1. Predictable lightness: oklch(60% ...) always appears at the same perceptual lightness regardless of hue
  2. Better gradients: hsl() gradients can look muddy in the middle; oklch() stays vibrant
  3. Accessibility: Easier to create accessible color pairs with consistent contrast ratios
  4. Wide gamut: Supports colors beyond sRGB (P3, Rec.2020)
/* Compare these gradients */
.hsl-gradient {
  background: linear-gradient(
    to right,
    hsl(0 100% 50%),
    hsl(120 100% 50%),
    hsl(240 100% 50%)
  );
  /* Notice the dark, muddy center */
}

.oklch-gradient {
  background: linear-gradient(
    to right,
    oklch(65% 0.25 29),   /* Red */
    oklch(65% 0.25 142),  /* Green */
    oklch(65% 0.25 264)   /* Blue */
  );
  /* Consistent perceptual lightness throughout */
}

color-mix(): Blending Colors in CSS

The color-mix() function allows you to blend two colors directly in CSS, similar to mixing paint:

.mixed-colors {
  /* Mix 50% blue with 50% white in sRGB */
  background-color: color-mix(in srgb, blue, white);
  
  /* 75% red, 25% black */
  color: color-mix(in srgb, red 75%, black);
  
  /* Mix in different color spaces */
  border-color: color-mix(in oklch, blue, white);
}

Syntax Breakdown

color-mix(in <colorspace>, <color1> [<percentage>], <color2> [<percentage>])
  • <colorspace>: srgb, oklch, hsl, lab, etc.
  • <color>: Any valid CSS color
  • <percentage>: How much of that color to use (defaults to 50%)
.practical-mixing {
  --brand-color: oklch(60% 0.2 264);
  
  /* Create hover state by mixing with white */
  --brand-hover: color-mix(in oklch, var(--brand-color), white 20%);
  
  /* Create active state by mixing with black */
  --brand-active: color-mix(in oklch, var(--brand-color), black 20%);
  
  /* Create disabled state */
  --brand-disabled: color-mix(in oklch, var(--brand-color), white 60%);
}

Relative Color Syntax

Relative color syntax lets you transform existing colors mathematically:

.relative-example {
  --base-color: oklch(60% 0.2 264);
  
  /* Increase lightness by 10% */
  background-color: oklch(from var(--base-color) l+10% c h);
  
  /* Reduce saturation to half */
  color: oklch(from var(--base-color) l c/2 h);
  
  /* Rotate hue by 180 degrees (complementary) */
  border-color: oklch(from var(--base-color) l c h+180);
}

The from Keyword

The from <color> syntax extracts components into variables you can modify:

.component {
  --input: oklch(70% 0.1 264);
  
  /* Create a palette from one base color */
  --input-bg: oklch(from var(--input) l+20% c h);
  --input-border: oklch(from var(--input) l-10% c h);
  --input-focus: oklch(from var(--input) l c+0.1 h);
  --input-error: oklch(from var(--input) l c h+30); /* Shift hue slightly */
}

Browser Support and Fallbacks

Current Support (as of 2026)

FunctionChrome/EdgeFirefoxSafariNotes
rgb() modern syntax✅ 90+✅ 103+✅ 15+Widely supported
hsl() modern syntax✅ 90+✅ 103+✅ 15+Widely supported
oklch()✅ 111+✅ 113+✅ 16.4+Modern browsers
color-mix()✅ 111+✅ 113+✅ 16.4+Modern browsers
Relative color syntax✅ 119+✅ 118+✅ 17+Very new

Providing Fallbacks

.with-fallbacks {
  /* Start with hex fallback */
  background-color: #3B82F6;
  
  /* Modern rgb() */
  background-color: rgb(59 130 246);
  
  /* Modern hsl() */
  background-color: hsl(217 91% 60%);
  
  /* Cutting-edge oklch() */
  background-color: oklch(60% 0.2 264);
}

/* Or use @supports */
@supports (background-color: oklch(0% 0 0)) {
  .modern-only {
    background-color: oklch(60% 0.2 264);
  }
}

Practical Tips for Adoption

  1. Use CSS custom properties: Define colors once and reference them everywhere
  2. Start with hsl(): It's well-supported and intuitive for creating palettes
  3. Experiment with oklch(): Use for new projects where browser support allows
  4. Leverage color-mix(): Great for hover/active states without defining new variables
  5. Test in both light and dark modes: Modern color functions make this much easier
:root {
  --hue: 217;
  --primary: hsl(var(--hue) 91% 60%);
}

[data-theme="dark"] {
  --primary: hsl(var(--hue) 91% 70%); /* Lighter for dark mode */
}