UI/UX Atlas
Color Intermediate

Wide Gamut & Display Color Spaces (P3, OKLCH)

Modern displays can render far more color than sRGB allows — learn how P3 and OKLCH unlock richer, more accessible, and future-proof color in production UI.

8 min read

The full lesson

Every color you pick in a design tool lives inside a color space — a mathematical container that defines which colors can be expressed and how they relate to each other. For decades, sRGB was that container, and it was good enough. But since 2017, most flagship phones, laptops, and monitors can display far more color than sRGB allows. If your palette still lives entirely in sRGB hex codes, you are leaving vivid blues, saturated greens, and vibrant oranges on the table — and possibly creating palettes that look uneven on devices that can actually show the difference.

Wide-gamut color spaces are no longer just a print-design concern. They are a practical front-end skill with real consequences for brand vibrancy, accessible contrast, and design-token architecture.

What “Color Space” Actually Means

A color space is a defined mapping between numbers and physical light. When you write #FF0000, the browser decodes it using the sRGB specification, which was standardized for cathode-ray-tube monitors in 1996. sRGB covers roughly 35% of all colors the human eye can see (the full visible range is called the CIE 1931 xy chromaticity diagram).

Display P3 — originally created by Apple and now part of the CSS Color Level 4 specification — covers roughly 45% of visible color, about 26% more than sRGB. That extra coverage lives mostly in highly saturated greens, cyans, and reds. Rec. 2020, used in HDR broadcast, covers about 75% of visible color, but no current consumer display gets close to hitting it fully.

Here are the three color spaces a UI designer needs to know today:

SpaceCoveragePrimary use
sRGB~35% of visibleLegacy web, all browsers (the default)
Display P3~45% of visibleModern screens; CSS color(display-p3 ...)
Rec. 2020~75% of visibleHDR video; not yet practical for UI

The sRGB Ceiling and Why It Matters Now

On an sRGB display, #FF0000 is the most saturated red the screen can show. On a Display P3 screen, the hardware can physically produce a distinctly more vivid red — but only if the CSS tells it to. If you keep using hex, the browser clips the color to the sRGB gamut even on capable hardware.

This gap is most visible in:

  • Brand colors defined as vivid greens, cyans, or reds — classic sRGB maxes out below what human vision and modern screens can handle.
  • Data visualizations where distinguishing similar hues is critical — wide-gamut colors give more perceptual distance between data series.
  • Buttons and interactive affordances — a more saturated primary action color draws the eye more effectively without needing higher contrast ratios or larger tap targets.
  • Photography and illustration — embedded wide-gamut images already display richer on P3 screens; your UI palette looks comparatively dull if it stays in sRGB.

On an sRGB-only display, a P3 color is clipped or tone-mapped to the nearest sRGB equivalent. This graceful degradation is built into the CSS Color 4 specification and works correctly in all evergreen browsers since 2022.

CSS Color Level 4: Syntax in Practice

CSS Color Level 4 introduced several functions that escape the sRGB container. The most important for UI work are color(), oklch(), and oklab().

Specifying a P3 color:

.button-primary {
  /* Fallback for very old browsers */
  background-color: #0066cc;
  /* Wide-gamut P3 value — richer blue on capable displays */
  background-color: color(display-p3 0.02 0.40 0.80);
}

The three numbers in color(display-p3 r g b) are linear 0–1 values in the P3 color space. A P3 value with all components in the 0–1 range is in-gamut for P3, but it may still be outside sRGB. The fallback is there for legacy support — modern browsers handle the clip automatically.

Using @supports for cleaner progressive enhancement:

.button-primary {
  background-color: #0066cc; /* sRGB fallback */
}

@supports (color: color(display-p3 0 0 0)) {
  .button-primary {
    background-color: color(display-p3 0.02 0.40 0.80);
  }
}

Browser support as of 2026: Chrome 111+, Safari 15+, Firefox 113+. Edge follows Chrome. This is safe to ship without a polyfill — older browsers silently use the cascade fallback.

OKLCH: Perceptually Uniform and Wide-Gamut

OKLCH is where modern color authoring gets genuinely powerful. It is a polar form of the OKLab color space, which was engineered in 2020 by Björn Ottosson specifically to fix the perceptual non-uniformity of LCH (itself an improvement on HSL).

The three axes of OKLCH:

  • L — Lightness from 0 (black) to 1 (white). Perceptually linear: L 0.5 looks halfway between black and white.
  • C — Chroma (saturation), from 0 (gray) to roughly 0.4 (maximum visible saturation). There is no fixed maximum — what is achievable depends on hue and lightness.
  • H — Hue angle in degrees (0–360), like HSL.
/* OKLCH syntax */
.chip {
  background-color: oklch(0.65 0.22 250); /* A clear blue, in-gamut on P3 */
  color: oklch(0.98 0.01 250);            /* Near-white with a faint blue tint */
}

Why OKLCH Beats HSL for Algorithmic Palettes

A common outdated habit is building tonal scales with Sass darken() / lighten() or by adjusting the L channel in HSL. Both produce perceptually uneven results: a 10% HSL lightness step looks visually different depending on the hue. The yellow-to-dark-yellow range is especially notorious — colors go muddy long before they are dark enough for contrast.

OKLCH’s perceptual uniformity means:

  1. A fixed L step looks the same across all hues.
  2. You can generate a full tonal scale programmatically (for example, L from 0.98 down to 0.10) and get visually even steps without hand-tweaking.
  3. Chroma is decoupled from lightness — you can brighten a color without accidentally desaturating it.

Modern design token tools (Radix Colors, Material Color Utilities, Tailwind’s new color system) have moved to OKLCH or OKLab as their internal generation space, even when they still export hex for compatibility.

Gamut Checking and Fallback Strategy

Not all OKLCH values land inside sRGB or even P3. A highly saturated OKLCH color (C above roughly 0.28 for most hues) is outside sRGB and may be outside P3 as well. Browsers clip out-of-gamut colors to the nearest in-gamut equivalent, which is usually acceptable, but can shift the hue slightly.

Practical gamut workflow:

  1. Author tokens in OKLCH — it gives you the widest expressive range and the best perceptual control.
  2. In your token build step (Style Dictionary, Theo, or a custom script), emit three outputs: oklch() for modern browsers, color(display-p3 ...) as a mid-tier fallback, and hex / rgb() for legacy.
  3. Use CSS custom properties and @supports to layer them progressively.
:root {
  --brand-primary: #1a6fe8;                          /* sRGB hex — universal */
  --brand-primary: color(display-p3 0.12 0.43 0.91); /* P3 — richer on wide-gamut */
  --brand-primary: oklch(0.60 0.21 263);             /* OKLCH — most expressive */
}

Because CSS custom property declarations cascade, the last valid value wins. On a browser that does not understand oklch(), it skips that line and keeps the P3 or hex value. This three-declaration pattern requires no @supports blocks for the property values themselves.

Impact on Accessibility and Contrast

Wide-gamut color does not automatically improve contrast, but it expands the design space for accessible palettes. A few key points:

Contrast ratios are color-space-aware. WCAG 2.2’s relative luminance formula is specified in sRGB. When a browser renders a P3 or OKLCH color, it tone-maps it to the display’s color profile before computing contrast. The computed contrast ratio you see in browser devtools reflects the actual rendered luminance — so standard WCAG 2.2 AA (4.5:1 for normal text, 3:1 for large text) still applies and is correctly measurable.

More saturated backgrounds complicate contrast. A vivid P3 cyan background can be bright enough to require dark text while also looking far more prominent than its luminance alone suggests. Always verify contrast on real P3 hardware, not just in a color picker.

APCA as a supplementary lens. The Advanced Perceptual Contrast Algorithm (APCA) is sometimes mentioned as a replacement for WCAG contrast. As of 2026 it remains a supplementary perceptual-quality tool, not an adopted standard. WCAG 2.2 AA remains the legal and compliance baseline. Use APCA to sanity-check edge cases, particularly for wide-gamut colors where sRGB-based luminance math can underestimate perceptual contrast.

Design Tool Workflow

Figma introduced native Display P3 support in 2023. When your Figma file’s color profile is set to Display P3, color pickers and hex values operate in the P3 space. Exported values use the color(display-p3 ...) CSS syntax.

Key workflow steps:

  • Set the document color profile to Display P3 under File settings.
  • Preview on a Display P3-capable monitor (most MacBooks since 2016, iPhones since X, most Android flagships since 2021).
  • Export color tokens using Figma’s Variables or a plugin like Tokens Studio. Verify that the exported JSON preserves the P3 or OKLCH syntax rather than downconverting to hex.
  • In the W3C Design Token Community Group (DTCG) stable JSON format, use the $type: color field and store values as oklch(...) strings so they round-trip without gamut loss.

Do

Author palette tokens in OKLCH for perceptual uniformity and wide-gamut range. Emit sRGB hex, P3, and OKLCH outputs from your token build step so every browser gets the best color it can show. Check contrast on real P3 hardware and in browser devtools. Use CSS custom properties with cascading declarations for a no-@supports fallback.

Don't

Don’t author palettes in HSL or by adjusting hex by hand — lightness steps are perceptually uneven across hues. Don’t assume a P3 color “just works” as a CSS custom property fallback without testing; invalid-at-computed-value-time silently kills the fallback chain. Don’t skip gamut checking — a highly saturated OKLCH value can be out-of-gamut for both sRGB and P3, producing unexpected clipping on older displays.

Practical Token Architecture for Wide-Gamut

Integrating wide-gamut into a three-tier token system (primitive → semantic → component) only changes the primitive layer. Semantic and component tokens remain hue-agnostic strings that point to primitives.

{
  "color": {
    "blue": {
      "500": {
        "$type": "color",
        "$value": "oklch(0.58 0.22 262)",
        "$extensions": {
          "srgb-fallback": "#1a6fe8",
          "p3-fallback": "color(display-p3 0.12 0.43 0.91)"
        }
      }
    },
    "action": {
      "primary": {
        "$type": "color",
        "$value": "{color.blue.500}"
      }
    }
  }
}

The $extensions field in DTCG format is the right place to attach platform-specific fallback values without polluting the main token graph. A Style Dictionary transform can read this and emit the correct CSS cascade pattern at build time.

Checking Support and Gamut at Runtime

JavaScript can detect wide-gamut display capability via the matchMedia API:

const isP3 = window.matchMedia('(color-gamut: p3)').matches;
const isWideGamut = window.matchMedia('(color-gamut: srgb)').matches === false;

You rarely need this in CSS-driven systems, but it is useful for canvas-based rendering (WebGL, 2D canvas color spaces), SVG fills driven by JavaScript, or analytics that track what percentage of your users have P3-capable displays. As of 2026, that is typically 70–80% of modern mobile users and 60–70% of desktop users.