All posts

    Color Spaces for Web Work: sRGB, Display P3, and OKLCH

    What a color space actually defines, why the same HEX looks different across screens, when Display P3 is worth the trouble, and why OKLCH is replacing HSL for palette math.

    A HEX value is not a color. It is three numbers that only become a color once you know which color space is interpreting them. Ignoring that is why a brand color can look correct on one monitor and washed out on another.

    What a color space defines

    Three things:

    • Primaries — the exact red, green and blue being mixed, expressed as coordinates in a device-independent reference space
    • White point — what counts as white, usually D65
    • Transfer function — the curve mapping stored values to light output, informally called gamma

    Change any one and the same numbers produce a different color. #ff0000 in sRGB and #ff0000 in Display P3 are both "maximum red," but P3's red primary is substantially more saturated.

    sRGB: the safe default

    sRGB has been the web's assumed space for decades. Its gamut is comparatively small, but that is also its strength — nearly every display can reproduce it, so an sRGB color looks roughly the same everywhere.

    Unless you have a specific reason otherwise, ship sRGB. It is what browsers assume for untagged content, what most CSS colors mean, and what your JPEG exports default to.

    Display P3: wider, and conditional

    Display P3 covers about 25% more gamut than sRGB, concentrated in saturated reds, greens and cyans. Most recent phones and laptops support it. In CSS you opt in explicitly:

    color: color(display-p3 1 0 0)

    Two cautions:

    It only helps for colors sRGB cannot express. Restating a color already inside sRGB in P3 gains nothing.

    Non-P3 displays must map it back. Browsers handle this, but the result may not match the sRGB color you would have chosen deliberately. Provide an sRGB fallback and treat P3 as an enhancement, not a baseline.

    Why the same image looks different in two apps

    Almost always an embedded profile problem. A JPEG or PNG can carry an ICC profile describing which space its numbers belong to. Color-managed software reads it; software that ignores it assumes sRGB.

    So an image tagged Adobe RGB, opened in an app that ignores profiles, looks desaturated — the numbers were meant for a wider space and are being interpreted as a narrower one.

    Relevant to browser-based tools: decoding an image to a canvas and re-encoding drops the ICC profile. For web delivery that is fine, because sRGB is the assumption anyway. For an image originally in Adobe RGB or ProPhoto, converting to sRGB before processing is the safe order.

    OKLCH: the one worth learning

    HSL has a known defect: equal lightness values do not look equally light. hsl(60, 100%, 50%) (yellow) is dramatically brighter than hsl(240, 100%, 50%) (blue) despite both claiming 50% lightness. Build a palette by fixing lightness across hues and the results are visibly uneven.

    OKLCH is a perceptually uniform space with three axes:

    • L — perceived lightness, 0 to 1, and it actually matches perception
    • C — chroma, roughly saturation, unbounded in principle
    • H — hue angle, 0 to 360

    Two practical consequences:

    Palette scales come out even. Fixing L and rotating H yields colors that genuinely look equally light, which is exactly what a design system needs across hue families.

    Gradients stop going muddy. linear-gradient(in oklch, ...) interpolates without the desaturated middle that plagues sRGB gradients.

    The tradeoff: OKLCH can express colors outside any display's gamut, so values need clamping, and browser support is newer. Design in OKLCH, ship HEX or sRGB for compatibility.

    A working policy

    1. Author and store in sRGB HEX for compatibility
    2. Do palette and scale math in OKLCH or, if tooling is limited, HSL with manual lightness correction
    3. Add Display P3 only for specific accent colors that need the extra saturation, always with a fallback
    4. Convert images to sRGB before processing, and expect profiles to be stripped by any canvas round-trip

    Try it

    Related reading: HEX vs RGB vs HSL covers the format layer above this one, and A Practical Guide to CSS Gradients has the gradient interpolation fixes.