Color Theory for Developers: HEX, RGB, HSL, and OKLCH Explained

Why Color Models Matter

Screens produce color by emitting light. Print produces color by absorbing it. Those two physical processes need two different models. When you write CSS, you are usually working in the additive, screen-based world, but the same value can be written several ways. Each format makes some operations easy and others awkward. Knowing the tradeoffs lets you pick colors deliberately instead of guessing.

Additive vs Subtractive Color

Screens start black and add red, green, and blue light to build a color. Mix all three at full intensity and you get white. This is the RGB additive model. Zero light is black, full light is white.

Print starts white and adds cyan, magenta, yellow, and key (black) ink to absorb light. Mix all four and you approach a muddy dark. This is the CMYK subtractive model.

For web work you live almost entirely in additive color. CMYK matters only when you hand assets to a print pipeline.

HEX Format

HEX is RGB written as hexadecimal. A pound sign, then two digits each for red, green, and blue, where each pair ranges from `00` to `ff` (0 to 255 decimal).


#ff8800   red=255, green=136, blue=0
#f80      shorthand when both digits match in each channel
#ff8800ff 8-digit form adds alpha (here fully opaque)

HEX is compact and copy-paste friendly. Designers pass Figma values around as HEX. Its weakness is that changing one channel shifts the color in a way humans do not perceive evenly. Bumping blue from `00` to `33` looks like a bigger jump in some colors than others.

RGB and RGBA

RGB spells out the same channels as decimal numbers. RGBA adds an alpha channel for opacity.


color: rgb(255, 136, 0);
color: rgb(255 136 0 / 50%);     /* modern space-separated syntax */
color: rgba(255, 136, 0, 0.5);   /* legacy comma syntax still works */

RGB is readable and explicit. Use it when you want each channel spelled out, or when you are reading values out of a devtools panel. It shares HEX's perceptual weakness: equal numeric steps do not look like equal visual steps.

HSL: Hue, Saturation, Lightness

HSL models how people think about color. Hue is an angle around the color wheel (0 to 360). Saturation is how intense the color is (0 to 100 percent). Lightness is how light or dark (0 percent black, 100 percent white, 50 percent pure color).


color: hsl(30, 100%, 50%);       /* a vivid orange */
color: hsl(30 100% 50% / 60%);   /* with alpha */

HSL makes common operations intuitive. To make a color lighter, raise lightness. To make it grayer, lower saturation. To get a complementary color, add 180 to the hue. Tints and shades become a single dial.

The catch: HSL is not perceptually uniform. A lightness of 50 percent in yellow looks much lighter than 50 percent in blue. Two colors with the same lightness value can look very different in brightness, which breaks contrast math and palette generation.

OKLCH: Perceptually Uniform Color

OKLCH is the modern answer. It is a perceptually uniform color space, meaning equal numeric changes look like equal visual changes. It uses three axes: Lightness, Chroma (saturation), and Hue.


color: oklch(70% 0.15 50);   /* a clean orange */
color: oklch(70% 0.15 50 / 0.6);

Why this matters:

  • Two colors with the same OKLCH lightness look equally bright, so picking a palette is reliable.
  • Mixing toward gray is linear, so generating a tonal scale produces smooth results.
  • You can rotate hue and keep perceived brightness constant, which HSL cannot do.

OKLCH is supported in all current major browsers. For new projects, prefer it when you generate palettes programmatically or need predictable contrast. Keep HEX and RGB for hand-tuning specific values that designers hand you.

Color Spaces: sRGB and Display P3

sRGB is the default color space of the web. Every HEX, RGB, and HSL value lives inside it. It covers about 35 percent of visible colors, which is enough for most work.

Display P3 (and the wider Rec. 2020) cover more colors, especially vivid greens, reds, and cyans that screens can now show. Modern devices, including most phones and laptops, render P3.

CSS lets you target P3 directly:


color: color(display-p3 1 0.4 0);

You can also write OKLCH and let the browser map it to the widest space the screen supports. The practical rule: define your brand colors in OKLCH, and they will look right on both sRGB and P3 displays without extra work.

WCAG Contrast and Accessibility

Text must stay readable against its background. WCAG defines contrast ratios that targets must meet.

ContentWCAG AAWCAG AAA
Normal text (under 18pt)4.5:17:1
Large text (18pt+ or 14pt bold)3:14.5:1
UI components and graphics3:13:1

Contrast is computed from relative luminance, which does not map cleanly to HSL lightness. A pure yellow and a pure blue can have very different luminance even at the same HSL lightness. This is exactly the problem OKLCH solves: matching OKLCH lightness gives you a much better starting guess at contrast.

For body text, aim above the 4.5:1 floor to leave room for anti-aliasing and user stylesheets. For large text and icons, 3:1 is the minimum, but more is safer. Test real pairs with a contrast checker rather than trusting the eye.

CSS Color Functions

Modern CSS ships functions that manipulate colors in the stylesheet, no preprocessor needed.

color-mix

Mix two colors in any color space.


color: color-mix(in oklch, #ff8800, #0044ff);
color: color-mix(in srgb, red 60%, blue);

Use it for hover states, borders, and tints. Mix toward a neutral gray to soften a color without recomputing it.

Relative colors

Derive a new color from an existing one with `from`.


color: oklch(from var(--brand) calc(l + 0.1) c h);   /* lighter brand */
color: rgb(from var(--brand) r g b / 0.5);           /* same color, half alpha */

This is huge for design systems. Define one brand token, derive hover, active, and disabled states from it, and never duplicate a value again.

color and lab/lch


color: lab(60% 40 30);
color: lch(60% 50 30);

`lab` and `lch` are the older siblings of OKLCH. They are perceptually uniform too, but OKLCH's hue angle matches expectations better, so prefer OKLCH for new work.

Picking a Format by Task

TaskBest format
Copying a value from a design toolHEX
Tuning one channel in devtoolsRGB
Making a tint or shade by handHSL
Generating a palette or scaleOKLCH
Hitting a contrast targetOKLCH, then verify
Wide-gamut brand colorOKLCH or color(display-p3)

The right format is the one that makes your current task easy. Switch between them as needed. A converter that handles all four lets you move without thinking.

Common Mistakes

  • Using HSL lightness for contrast math. Two colors at 50 percent HSL lightness can fail WCAG. Use relative luminance or OKLCH lightness instead.
  • Storing brand colors in RGB and then trying to generate variants. Relative color functions work across formats, but OKLCH derivations look smoother.
  • Ignoring wide gamut. Photos and gradients can clip on P3 displays if you assume sRGB everywhere. Test on a wide-gamut screen.
  • Picking colors by eye only. Perception adapts to surrounding colors. Verify with a contrast checker and a numeric palette tool.
  • Hardcoding derived values. Use relative colors to derive hover and disabled states from a single token, so updates stay in sync.

A Practical Workflow

1. Capture the brand color from design as HEX.

2. Convert it to OKLCH to understand its lightness, chroma, and hue.

3. Build a tonal scale by stepping OKLCH lightness while holding chroma and hue steady.

4. Pick text colors that clear WCAG against each step, verifying with a contrast checker.

5. Store the base token as a CSS variable and derive states with `color-mix` and relative colors.

6. Test the palette on both an sRGB and a wide-gamut display.

This workflow scales from a single landing page to a full design system, and it produces palettes that look consistent and meet accessibility targets by construction rather than by accident.

Convert Between Formats

Moving between HEX, RGB, HSL, and OKLCH by hand is slow and error-prone. Drop any value into the [color code converter](/en/color-code-converter) to get every format at once, with alpha support. It is the fastest way to take a designer's HEX, check the OKLCH axes, confirm the WCAG contrast, and paste the right value into your CSS.