Color Spaces Explained: RGB, HSL, CMYK, and HEX for Developers

Color Spaces Explained: RGB, HSL, CMYK, and HEX for Developers

Color looks deceptively simple from the outside. You pick a shade, you use it. But underneath every design system, browser, and print workflow is a set of choices about how color is represented mathematically — and those choices affect everything from whether your button looks right on a wide-gamut display to whether your brand colors survive printing.

Why Color Spaces Exist

Screens, printers, and human eyes all work differently. Screens emit light — mixing red, green, and blue sources to produce color. Printers absorb light — layering ink pigments that each block certain wavelengths. And our eyes are most sensitive to green, less so to red and blue, perceiving lightness non-linearly.

A color space is a formalized mapping from numbers to perceived colors. The same number means something different in different spaces, which is why "#ff0000," "RGB(255,0,0)," and "CMYK(0,100,100,0)" all mean approximately the same red — but not exactly the same, and not on all devices.

RGB: The Screen Model

RGB represents colors as combinations of Red, Green, and Blue channels. It's an additive color model: adding all three at full intensity produces white; zero on all three is black. Screens emit no light at zero, then mix colored light toward white at maximum.

In CSS and most web contexts, each channel runs from 0 to 255 (8-bit):

color: rgb(255, 128, 0); /* orange */

You'll also see 0–1 float representation in shader code, SVG, and some graphics APIs:

vec3 color = vec3(1.0, 0.5, 0.0); /* same orange in GLSL */

HEX: RGB in Base 16

Hex color notation is just RGB with each channel expressed as a two-digit base-16 number:

color: #ff8000; /* same orange: ff=255, 80=128, 00=0 */

Three-digit shorthand expands each digit to two of the same:

color: #f80; /* expands to #ff8800 — not the same orange */

Eight-digit hex adds an alpha channel as the last two digits:

color: #ff800080; /* orange at 50% opacity */

Hex is ubiquitous in web tooling because it's compact and copy-pasteable. The values are identical to RGB — it's purely a notation difference, not a different color model. Use the Color Converter to translate between formats instantly.

HSL: Designed for Humans

HSL separates color into three perceptually meaningful axes:

  • Hue — the color itself, expressed as degrees around a color wheel (0° = red, 120° = green, 240° = blue, 360° = red again)
  • Saturation — how vivid the color is (0% = gray, 100% = fully saturated)
  • Lightness — how light or dark (0% = black, 50% = the full color, 100% = white)
color: hsl(30, 100%, 50%); /* orange */

HSL is far more intuitive for making design decisions. Want a lighter version of your brand color? Increase L. Want a muted version? Decrease S. Want the complementary color? Add 180° to H.

/* Generate a hover state programmatically */
:root {
  --brand-h: 30;
  --brand-s: 100%;
  --brand-l: 50%;
}

.button {
  background: hsl(var(--brand-h), var(--brand-s), var(--brand-l));
}
.button:hover {
  background: hsl(var(--brand-h), var(--brand-s), calc(var(--brand-l) - 10%));
}

This is much cleaner than trying to darken #ff8000 by doing hex arithmetic.

HSB / HSV: Not the Same as HSL

HSB (Hue, Saturation, Brightness) and HSV (Hue, Saturation, Value) are the same model with different names — common in Photoshop and design tools. They look similar to HSL but behave differently.

The key difference: in HSL, maximum lightness (100%) always gives you white regardless of saturation. In HSB/HSV, maximum value (100%) gives you the full vivid color. Black is at Value=0, but there's no equivalent to HSL's 100% lightness "white" at full saturation.

Photoshop's color picker uses HSB. CSS uses HSL. This trips up designers who wonder why the CSS value they transcribed from Photoshop looks slightly different in the browser.

CMYK: Subtractive Color for Print

CMYK stands for Cyan, Magenta, Yellow, and Key (Black). It's a subtractive model: each ink layer absorbs certain wavelengths of reflected light. Layering all four at 100% produces black (theoretically — in practice it produces a muddy dark brown, which is why a separate K channel exists for clean black text).

C: 0%  M: 100%  Y: 100%  K: 0%  → red (absorbs cyan, passes red)
C: 100%  M: 0%  Y: 0%  K: 0%    → cyan (absorbs red)

CMYK does not exist in CSS. It's a print model. Web browsers work exclusively in RGB. When you send a design to a printer, your software converts from RGB to CMYK, which can shift colors noticeably — especially vivid blues and oranges that exist in RGB but can't be reproduced with CMYK inks.

If you're designing for both screen and print, define your brand colors in a device-independent space (like LAB or Pantone), then convert separately to sRGB for screens and CMYK for print. Don't just copy the hex into InDesign.

Color Gamuts: sRGB, P3, Rec. 2020

A gamut is the range of colors a color space can represent. Not all colors are expressible in every space.

  • sRGB — the standard for the web since 1996. Covers about 35% of all perceivable colors (the CIE 1931 xy chromaticity diagram area).
  • Display P3 — about 25% wider than sRGB in total color volume. The default gamut on modern Apple displays, most high-end Android flagships, and newer monitors. Can represent more vivid greens and reds.
  • Rec. 2020 — intended for HDR video production. Covers roughly 75% of perceivable color. Most consumer displays can't fully reproduce it yet.

CSS now supports wide-gamut color with the color() function:

/* Vivid green only achievable in P3, not sRGB */
color: color(display-p3 0.0 1.0 0.0);

Use the @media (color-gamut: p3) media query to target wide-gamut displays specifically:

.highlight {
  background: hsl(130, 80%, 45%); /* sRGB fallback */
}
@media (color-gamut: p3) {
  .highlight {
    background: color(display-p3 0.1 0.95 0.3);
  }
}

On a P3 display, that second value looks noticeably more vivid. On a standard sRGB monitor, both look similar.

CSS Color Syntax Options

CSS supports several notations for the same color space:

/* All equivalent for the same red */
color: #ff0000;
color: rgb(255, 0, 0);
color: rgb(100% 0% 0%);   /* space-separated, CSS Color 4 */
color: hsl(0, 100%, 50%);
color: hsl(0deg 100% 50%);
color: oklch(62.8% 0.258 29.23); /* perceptually uniform, CSS Color 4 */

oklch and oklab are newer additions to CSS that use a perceptually uniform color model — meaning equal numeric steps produce equal perceived changes in color. These are increasingly useful for generating accessible color palettes programmatically. The W3C CSS Color 4 specification covers the full syntax.

CSS named colors (red, cornflowerblue, rebeccapurple) are just aliases for specific hex values, no different from the hex directly.

Converting Between Spaces

The math behind conversions is well-defined. RGB to HSL, for example:

function rgbToHsl(r, g, b) {
  r /= 255; g /= 255; b /= 255;
  const max = Math.max(r, g, b), min = Math.min(r, g, b);
  let h, s, l = (max + min) / 2;

  if (max === min) {
    h = s = 0; // achromatic
  } else {
    const d = max - min;
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    switch (max) {
      case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
      case g: h = ((b - r) / d + 2) / 6; break;
      case b: h = ((r - g) / d + 4) / 6; break;
    }
  }
  return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
}

Rather than doing this manually, Color Converter handles all common conversions — hex to RGB, RGB to HSL, and back — with clipboard-ready output. For more on how color variables interact with component systems, CSS Custom Properties Explained covers the cascade mechanics that make token-based color systems practical.

You don't need to think about color spaces on every project. But when your brand green looks washed out on the client's new monitor, or your print designer says your colors "aren't printing right," the answer is almost always somewhere in here.