Early HTML: simple color attributes
In the very first versions of HTML, color support was minimal:
- Colors were applied directly on elements via attributes like
bgcolor,link,vlink,alink. - Values were either basic names (
red,blue,green) or hex codes (#ff0000).
<body bgcolor="#ffffff" text="#000000" link="blue" vlink="purple">
There were no stylesheets yet, no variables, no themes – just inline color choices that affected the entire page.
Hex codes and 8‑bit color limitations
Hex codes quickly became the dominant way to define colors:
color: #ff0000; /* red */ background: #003366;
Each pair of hex digits describes the intensity of red, green and blue:
#RRGGBB, values from00(0) toff(255)- 256 possible values per channel → over 16 million combinations
However, in the 1990s and early 2000s many monitors and graphics cards ran in 8‑bit color (256 colors at once). To avoid banding and unexpected dithering, designers often tried to stick to colors that looked consistent across limited palettes.
The 216 "web‑safe" colors
To cope with 256‑color displays, the idea of "web‑safe colors" emerged:
- A palette of 216 colors defined by using only these hex channel values:
00,33,66,99,cc,ff - Every combination of those six values for R, G and B was considered "safe"
The theory
- Browsers on different systems would map these 216 colors directly to their hardware palettes.
- Designers who stuck to this set could be confident their colors wouldn't dither or shift too much.
In practice, web‑safe colors were always a compromise:
- They limited creative choice.
- Actual device support and palettes varied.
As true 24‑bit color became standard, the need for this restriction disappeared. Today, "web‑safe colors" are mostly a historical curiosity – but you may still see references in old tutorials and design tools.
CSS Color Level 1–3: named colors, rgb() and hsl()
As CSS matured, the way we write colors became more flexible.
Named colors
CSS adopted and standardised a list of named colors, many inherited from X11:
color: tomato; background: darkslategray; border-color: cornsilk;
There are 147 such names plus special keywords like transparent and currentColor.
rgb() and rgba()
Instead of hex, you could write explicit RGB components:
color: rgb(255, 0, 0); background: rgba(0, 0, 0, 0.5); /* 50% transparent black */
This made it easier to tweak individual channels or specify opacity.
hsl() and hsla()
CSS Color Level 3 introduced HSL (Hue, Saturation, Lightness):
color: hsl(210, 40%, 98%); background: hsla(210, 100%, 50%, 0.1);
HSL matches how designers think:
Hue
Which color — an angle on a color wheel
Saturation
How intense or muted the color is
Lightness
How light or dark the color is
It made building simple color scales and tints more intuitive than with raw RGB.
From sRGB to wide‑gamut (Display‑P3 and beyond)
For a long time, the web assumed a single color space: sRGB.
- Designed in the 1990s to match typical CRT monitors.
- Reasonably small gamut, but good for interoperability: the same hex code looked similar on most displays.
As displays improved – especially high‑end phones, tablets and monitors – many devices began to support wider gamuts such as Display‑P3:
- Able to show more saturated reds and greens
- Roughly corresponds to modern cinema and Apple display capabilities
The problem
- Hex,
rgb()and classichsl()do not say which color space they refer to. By default they're sRGB. - To use wider gamuts on the web in a controlled way, CSS needed new syntax and clearly defined color spaces.
Modern CSS: color() syntax and new spaces
CSS Color Level 4+ introduces a more flexible function:
color: color(display-p3 0.9 0.2 0.3);
Key ideas:
- Explicit color space:
srgb,display-p3,rec2020,lab,lch,oklab,oklch, etc. - Components are defined according to that space's rules, not always 0–255.
- Browsers can clamp or convert if a device can't display the full gamut.
/* Wide-gamut P3 color */ background: color(display-p3 0.85 0.15 0.2); /* CIELAB - perceptual color space */ color: lab(60% 40 20); /* LCH (Lightness, Chroma, Hue) */ color: lch(65% 60 250);
This opens the door to
- richer, more vivid colors on capable displays
- more accurate conversions and gradients
- color work based on perceptual spaces instead of raw RGB
Perceptual models and oklch() / oklab()
Recent research led to new perceptual models such as OKLab and OKLCH. They aim to:
- make numeric distances correspond more closely to perceived differences
- produce smoother gradients and better‑behaved lightness curves
- work well across both sRGB and wide‑gamut spaces
CSS now supports them via:
color: oklab(0.7 0.1 0.02); color: oklch(0.7 0.12 230);
L — Lightness
0 to 1, from black to white
C — Chroma
Colorfulness — 0 is neutral gray, higher values are more vivid
H — Hue
Angle in degrees around the color wheel
Benefits for UI
- You can create scales by stepping lightness and chroma in consistent increments.
- Dark and light themes can be tuned to have similar perceived contrast.
- Gradients avoid the "muddy middle" common with plain RGB interpolation.
Frameworks and tools (including newer Tailwind releases and design systems) increasingly build palettes using OKLCH or related perceptual models behind the scenes.
Where we are today
Modern web color has come a long way from bgcolor="#ff0000":
- Hex and
rgb()remain widely used for compatibility and simplicity. hsl()is still popular for hand‑tuned palettes.- Data‑driven and design‑system teams are adopting explicit color spaces with
color(), OKLCH/OKLab for palette generation, and design tokens to separate semantic roles from raw values. - Wide‑gamut displays are becoming common, so P3 colors are more useful every year.