CSE 134B Web Client Languages
  1. Presentation
  2. Color
  3. How color works

Presentation · Color 02

How color works

A screen mixes red, green and blue light; a color space pins down exactly which red, which green, which blue and which white; and the eye judges the result in its own uneven way. The gap between the numbers in a style sheet and what a person sees explains most of the web's color history, from the 216 colors of the web-safe palette to the move from hex and HSL to oklch(). This page covers the science a developer needs, and no more.

Why color matters, page 01, argued that color is the least reliable channel on a page. This page explains why. Color in CSS, the next page, tours every notation in detail; here the notations appear only as far as the ideas need them. Two demos let you see the ideas at work: the same gradient blended in four color spaces, and a photograph forced into the 216 colors of the 1990s.

1. Three cones, three numbers

Light is a mix of wavelengths. Sunlight holds all of the visible ones, from violet near 380 nanometers to deep red near 700; a red LED emits a narrow band. A complete description of a light would be its spectrum: how much power it carries at every wavelength, hundreds of numbers.

The eye throws most of that away. In daylight, color vision rests on three kinds of cone cells in the retina, often called L, M and S for long, medium and short wavelengths. Their sensitivities overlap widely and peak roughly near 560, 530 and 420 nanometers. Each kind of cone reports a single number, how strongly it is stimulated, and those three signals are all the brain receives about color. Everything else about the spectrum is lost before thinking begins.

That is why three numbers can describe a color. It is not a property of light, which has no "three" in it, but of human vision, which is trichromatic. Screens, cameras, image formats and CSS all inherit the three: red, green and blue for a display, three channels in a JPEG, three numbers in rgb() and in oklch().

Metamerism: different light, same color

Because the eye reduces a spectrum to three signals, two very different spectra can produce the same three signals and look identical. Such a pair is called a metamer, and the effect metamerism. The yellow on a screen is the everyday example. A screen has no yellow light at all; it lights its red and green subpixels together, and the mixture stimulates the L and M cones in about the same proportions as light near 580 nanometers would. The eye cannot tell the difference, so it sees yellow.

Every color on every screen is a metamer of this kind, which is what makes displays possible. Metamerism also has a cost that designers meet in practice: two samples that match under one light may not match under another, because the light changes the spectra they reflect. A brand color that matches on a laptop may not match on a printed banner. And because cone sensitivities vary a little from person to person, a match for one person can be a slight mismatch for another.

Not everyone has three working cone types. About 8 percent of men and 0.5 percent of women of Northern European descent have some form of color vision deficiency, most often a shifted or missing L or M cone, which merges reds and greens. Page 08, Color for people, takes that up; for now, the point is that "three numbers" describes typical vision, and a palette must still work for readers whose vision reduces color to two.

2. Adding light, subtracting ink

There are two ways to make color, and they run in opposite directions.

  • Additive color starts from black and adds light. A screen's pixel is dark until its red, green and blue subpixels glow, and the more each glows, the brighter the result. Red and green make yellow, all three at full strength make white. The primaries are red, green and blue, which is why web colors are written as RGB.
  • Subtractive color starts from white paper lit by white light and takes light away. Each ink absorbs part of the spectrum and lets the rest reflect. Cyan ink absorbs red, magenta absorbs green, yellow absorbs blue; together they absorb nearly everything. Printers add black ink, the K in CMYK, because three colored inks make a muddy dark brown rather than a true black.

The two systems overlap but do not match. Many saturated screen colors, such as a vivid blue or a glowing green, cannot be printed with ordinary inks, and some printed colors, such as certain deep cyans, are hard for a screen to show. So a color chosen on a screen is a request, not a guarantee, when it reaches paper. Page 07, Displays and environments, returns to print style sheets with this in mind. CSS has no CMYK notation that browsers support: the web is an additive medium, and every color in a style sheet describes light.

3. Models and spaces

Two words are easy to confuse, and the difference explains much of CSS color.

A color model is a way of arranging numbers: a coordinate system. RGB arranges colors as amounts of three primaries in a cube. HSL rearranges the same cube as a cylinder of hue, saturation and lightness. HWB describes it as a hue mixed with white and black. A model says nothing about which actual colors the numbers mean. "Full red" in the RGB model is only "as red as this device's red can be".

A color space fixes the meaning. For an RGB space, that takes three things:

  • The primaries: exactly which red, which green and which blue, given as positions on a standard map of human color vision.
  • The white point: exactly which white the three make at full strength. Most screen standards use D65, a standard daylight with a color temperature near 6500 kelvin.
  • The transfer function: how the stored numbers relate to the amount of light. Section 5 shows why this matters.

sRGB, Display P3 and Rec. 2020 are all spaces built on the same RGB model with different primaries. So rgb(255 0 0) in sRGB and color(display-p3 1 0 0) are both "full red", and they are different colors: the P3 red is redder. In CSS, hex, the named colors, rgb(), hsl() and hwb() always mean sRGB. That is why they cannot name the extra colors of a modern phone. The newer notations either name their space outright, as color() does, or describe colors in a space built on human vision rather than on a display, as lab() and oklch() do.

The map those spaces are drawn on comes from 1931, when the CIE, the International Commission on Illumination, published a standard observer: a table of how an average person matches each wavelength with three primaries, measured in color-matching experiments. From it come the CIE XYZ space, which can describe any color a typical person sees, and the chromaticity diagram in the next section. Every color space in CSS is defined, in the end, by how it converts to XYZ.

4. Gamut

A gamut is the set of colors a space or a device can produce. An RGB space's gamut is set by its primaries: every color it can make is some mix of the three, so on a chromaticity diagram the gamut is the triangle with the primaries at its corners. The horseshoe around the triangles is the edge of human vision, the colors of single wavelengths, and every color a person can see lies inside it.

sRGB, Display P3 and Rec. 2020 as nested triangles on the CIE 1931 chromaticity diagramA chart with x from 0 to 0.8 across and y from 0 to 0.9 up. A horseshoe-shaped curve, the spectral locus, marks the chromaticities of single wavelengths of light, labeled in nanometers from 460 at the lower left, up through 500 and 520 at the top left, down through 560, 580 and 600 to 700 at the right; a straight line, the line of purples, closes it along the bottom. Inside sit three triangles that share a white point, D65, at x 0.3127, y 0.3290. The smallest, sRGB, has its corners at red 0.64, 0.33; green 0.30, 0.60; blue 0.15, 0.06. Display P3 shares sRGB's blue corner and reaches further toward red, at 0.68, 0.32, and much further toward green, at 0.265, 0.69. Rec. 2020 is the largest, with corners at 0.708, 0.292; 0.170, 0.797; and 0.131, 0.046, close to the curve. Every color a screen of each standard can show lies inside its triangle; the area outside all three is visible but beyond them.00.20.40.60.8x0.20.40.60.8y460480490500520 nm540560580600620700line of purples: no single wavelengthD65 whitesRGBP32020Three gamuts, one whitesRGB, solid: the web defaultDisplay P3, dashed: 1.36 ×Rec. 2020, dotted: 1.89 ×(areas relative to sRGB)Curve: the spectral locus, everysingle wavelength, in nanometers
Gamut on a chromaticity diagram. The corners are the published primaries (sRGB, IEC 61966-2-1; Display P3; Rec. 2020, ITU-R BT.2020), as listed in CSS Color 4, plotted on the CIE 1931 2° observer data. The diagram shows hue and saturation only, not lightness, and its areas do not match what the eye sees as equal steps; read it for shape, not for percentages.

Read the diagram from the inside out. sRGB, the smallest triangle, was proposed in 1996 by Hewlett-Packard and Microsoft as a default color space for the internet, based on the cathode-ray-tube monitors of the time; CSS1 made it the default a month later. Display P3 keeps sRGB's blue, pushes its red further out and its green much further, and, per CSS Color 4, "modern displays, TVs, laptop screens and phone screens are able to display all, or nearly all" of it. It takes the primaries of the cinema standard DCI-P3 and keeps sRGB's white and transfer curve. Rec. 2020, the standard for ultra-high-definition television, puts its primaries almost on the edge of vision. CSS Color 4 notes that current displays cannot reproduce all of it.

The two numbers in the legend are areas on this diagram, computed from the primaries: P3's triangle is about a third larger than sRGB's, and Rec. 2020's nearly twice as large. Treat them as a sense of scale only: the diagram leaves out lightness, and equal areas on it are not equal to the eye. What it does show is the shape of the gain: most of what P3 adds is greens and reds, and that is where a wide-gamut screen looks most different.

Two practical consequences follow. A color outside sRGB, named in oklch() or color(display-p3 …), is valid CSS, but a screen that cannot show it must replace it with one it can, which Color in CSS calls gamut mapping and measures. And a design that depends on a vivid color only some screens can show depends on the reader's hardware, which is why page 07, Displays and environments, treats wide gamut as an enhancement.

5. Encoded and linear light

An sRGB value such as #808080, 128 out of 255 in each channel, sits in the middle of the numbers. It does not send half as much light as white. The numbers in sRGB are encoded: before the screen shows them, they pass through a curve, the transfer function, that turns them into amounts of light.

The sRGB transfer curve: encoded value against lightA square chart. Across: the encoded sRGB value, from 0 to 255 in each channel. Up: the light the screen sends, from 0 to 100 percent of white. A dashed straight diagonal shows what a linear encoding would be. The sRGB curve sags below it: it rises slowly at first and steeply at the end. Marked on the curve: the encoded value 128, hex 80, sends only 21.6 percent of white's light. Half the light, 50 percent, needs the encoded value 188, hex BC. So #808080 is not half as bright as white in light, and averaging encoded numbers gives a darker color than averaging light.128 (80)188 (BC)0255encoded value in each channel (hex in parentheses)21.6%50%100%light sentstraight line: linearNumbers are not light#808080 is the middle of the numbers,and sends only 21.6% of white's light.Half of white's light is #bcbcbc.The curve spends more numbers on darktones, where the eye notices small steps,so 8 bits per channel suffice for most images.Math on colors (mixing, blending, blur)is physically right only on linear values:decode, compute, encode again.
The sRGB transfer function, drawn from its formula: a short straight segment near black, then a power of 2.4, close overall to a gamma of 2.2. Encoded values are good for storage and poor for arithmetic.

The curve exists for a good reason. The eye is much more sensitive to differences between dark tones than between light ones, so a code that spaced its 256 steps evenly in light would waste most of them on bright tones nobody can tell apart and leave visible steps in the shadows. The sRGB curve spends more of its numbers on dark tones, which is close to how the eye works, and 8 bits per channel are enough for most images as a result.

The cost is that encoded numbers are not quantities of light, so arithmetic on them is not physics. Averaging two encoded values does not average their light. Mixing, blurring and gradients are all averages, and done on encoded values they come out too dark in the middle. For years, browsers did exactly that for gradients, and older design tools did it for blends and blurs. The fix is to decode to linear light, compute, and encode again: CSS calls that space srgb-linear.

:root { --blue: oklch(45.2% 0.313 264.05); /* sRGB's pure blue, #0000ff */ --yellow: oklch(96.8% 0.211 109.77); /* sRGB's pure yellow, #ffff00 */ } .encoded { background-image: linear-gradient(to right in srgb, var(--blue), var(--yellow)); } .light { background-image: linear-gradient(to right in srgb-linear, var(--blue), var(--yellow)); } .oklab { background-image: linear-gradient(to right in oklab, var(--blue), var(--yellow)); } .oklch { background-image: linear-gradient(to right in oklch, var(--blue), var(--yellow)); }

Linear light is the right space for physics, but it is not the right space for looks. A blend that averages light correctly can still look wrong, because the eye does not judge brightness in proportion to light either. The demo shows all four, with numbers.

Which space a gradient uses by default depends on how its colors are written. CSS Color 4 makes Oklab the default for interpolation, but adds a rule for compatibility: when every color is a "legacy" sRGB notation (hex, a name, rgb(), hsl() or hwb()), browsers interpolate in encoded sRGB, as they always did. So linear-gradient(#0000ff, #ffff00) still goes gray in the middle, while the same gradient written with oklch() stops is blended in Oklab. Deriving color, page 04, covers choosing the space and the direction a hue takes around the wheel.

6. Perceptual uniformity

Linear light describes what the screen emits. Designers need a space that describes what a person sees, where equal steps in the numbers look like equal steps in color. A space with that property is called perceptually uniform, and no space achieves it perfectly, because vision is too complicated. The history of color science is a series of better approximations.

  • CIELAB, officially the CIE 1976 L*a*b* color space, was the first widely adopted attempt. It describes a color by a lightness, L*, and two opponent axes: a, from green to red, and b, from blue to yellow. CIELAB is still the standard in print, paint and color measurement, and CSS writes it as lab(). Its known weakness is hue: as a blue gets lighter or less colorful in CIELAB, it drifts visibly toward purple.
  • Oklab, published by Björn Ottosson on 23 December 2020, keeps the same shape, a lightness and two opponent axes, but is fitted to newer perceptual data so that hue holds steady, and its math is simple enough to run per pixel. CSS adopted it within a few years, as oklab().
  • LCH and OKLCH are the same two spaces in polar form. Instead of a and b, they give chroma, the distance from gray, and hue, the angle around it. Lightness, chroma, hue: three knobs a person can turn on purpose. CSS writes them as lch() and oklch().

Polar forms matter because they put the coordinates where people think. "The same red, lighter" is a change to L alone. "The same red, duller" is a change to C alone. "A green as light as this red" keeps L and C and changes H. HSL offered similar knobs in the late 1970s, but it is arithmetic on the RGB cube, not a model of vision, and its "lightness" ignores how differently the eye responds to each primary. hsl(60 100% 50%) and hsl(240 100% 50%) share a lightness of 50%, and the yellow sends thirteen times the light of the blue.

Color in CSS shows how to read oklch() values, and its lightness strip demo puts twelve hues at one lightness in both notations, with the measured luminance of each swatch printed underneath.

Perceptual uniformity is what makes a token system predictable. If lightness means what it looks like, a set of buttons at the same L carries the same visual weight, a scale of tints can step evenly from light to dark, and a hover state "ten points darker" is darker by the same amount for every brand color. Choosing and pairing, page 05, builds palettes on that.

7. Luminance is not lightness

Two quantities sound alike and are used for different jobs.

  • Relative luminance, written Y, is physical. It measures how much light a color sends, weighted by the eye's sensitivity to each wavelength, on a scale from 0 for black to 1 for white. It is computed from linear light, so it adds up: two lights together have the sum of their luminances.
  • Lightness, the L of CIELAB and OKLCH, is perceptual. It estimates how light a color looks, on a scale meant to have even steps. It is roughly the cube root of luminance, which is why a small amount of light already looks fairly light.

The numbers show the gap. A gray that looks halfway between black and white, CIELAB lightness 50, has a luminance of about 0.18: it sends less than a fifth of white's light. OKLCH puts its own 50% gray, oklch(50% 0 0), lower still, at a luminance of 0.125. The light-average gray, #bcbcbc with luminance 0.5, has an OKLCH lightness of 79.5% and looks well past the middle.

The contrast ratios of WCAG 2, which page 08, Color for people, uses throughout, are built on luminance. The formula linearizes each sRGB channel, weights red, green and blue by 0.2126, 0.7152 and 0.0722, the eye's unequal sensitivity, and compares two colors as (Ylighter + 0.05) / (Ydarker + 0.05).

// Decode one 0–255 sRGB channel to linear light. const linear = (v) => { const c = v / 255; return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; }; // Relative luminance: green counts ten times as much as blue. const luminance = ([r, g, b]) => 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b); const contrast = (a, b) => { const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x); return (hi + 0.05) / (lo + 0.05); }; contrast([0, 0, 238], [255, 255, 255]); // 9.40: the default link blue on white

So OKLCH's L and WCAG's luminance are related, both rising from black to white, but not interchangeable. Two colors with the same OKLCH lightness have similar luminance, and the lightness strip shows how close. But a fixed gap in L is not a contrast guarantee: among OKLCH grays, a 40-point gap gives anywhere from 2.3:1 (black against 40%) to about 5.1:1 (30% against 70%), depending on where it sits, so always compute the ratio from luminance.

Until May 2021 the WCAG formula used 0.03928 where the sRGB standard uses 0.04045, taken from an older sRGB draft. The WCAG 2.x drafts were corrected then, and WCAG 2.2 records the change in a note, adding that it "has no practical effect" on the results.

8. A short history of web color

The web has been naming colors since 1993, and every notation in CSS today was added to fix a limit of the ones before it. The table gives the dates; the subsections tell the stories that still shape what you write.

Web color, 1993 to 2026
WhenWhat changed
April 1993NCSA Mosaic 0.13 makes links blue and underlined, and visited links dark purple
1995Netscape 1.1 lets authors set bgcolor, text, link, vlink and alink on <body>, with defaults of blue, purple and red; its documentation gives only six hex digits, #rrggbb
Late 1995Internet Explorer 2.0 accepts color names as well as hex in the same attributes; its examples are red and fuchsia
1996Internet Explorer 3 and Netscape Navigator accept about 140 names, the X Window System's plus the sixteen VGA colors, with no standard behind them; for 8-bit screens, both draw from nearly the same 6 × 6 × 6 color cube
1996Lynda Weinman names and publishes the 216-color "browser-safe" palette, on her site and in her books
November 1996Hewlett-Packard and Microsoft propose sRGB as "a standard default color space for the internet"
December 1996CSS1 brings color, background-color, #rrggbb, the new #rgb shorthand, rgb(), and sixteen keywords "taken from the Windows VGA palette", a small subset of the names browsers accepted
January 1997HTML 3.2 standardizes the same sixteen color names and the <body> color attributes
October 1999sRGB becomes an international standard, IEC 61966-2-1
September 2000Lehn and Stern find that only about 22 of the 216 web-safe colors stay exact in every setup they test, once 16-bit displays are included
Any yearHTML's forgiving attribute parser turns almost any string into a color: bgcolor="chucknorris" is a dark red
June 2011CSS Color 3 becomes a Recommendation: hsl(), rgba(), hsla(), transparent as a color, and the 147 names SVG 1.0 had standardized in 2001
June 2014rebeccapurple (#663399) is added in memory of Rebecca Meyer
2016 and 2020WebKit proposes, then explains, wide-gamut color in CSS: color(display-p3 …)
December 2020Björn Ottosson publishes Oklab
May 2023lab(), lch(), oklab(), oklch(), color() and color-mix() are in every major engine, with Firefox 113
2024light-dark() (May) and relative color syntax (September) reach every engine
2026contrast-color() (April) and alpha() (September) reach every engine

The one color convention every web user knows is older than any color standard. The earliest browsers did not use it: Tim Berners-Lee's WorldWideWeb marked links with an underline, and ViolaWWW drew them black and underlined. On 12 April 1993, Marc Andreessen announced NCSA Mosaic 0.13 with "blue and single solid underline for unvisited, dark purple and single dashed underline for visited". The defaults stuck.

Why blue is less certain than the stories told about it. Berners-Lee's FAQ calls it "just a default" and guesses: "blue is the darkest color and so threatens the legibility least." Mozilla's 2021 history of the question notes that the HyperTIES hypertext system already used colored links, cyan ones, in 1983, and its answer opens with "No one knows" before offering theories.

Netscape 1.1, in 1995, let authors change the colors with attributes on <body>. Netscape's documentation gives the value as "#rrggbb", "a hexadecimal red-green-blue triplet", and names came later. Besides bgcolor and text came link, vlink for visited and alink for a link being clicked, defaulting to blue, purple and red. HTML 3.2 standardized them in 1997, and CSS replaced them with :link, :visited and :active. The defaults live on in the HTML standard's rendering section, which every browser follows:

:link { color: #0000EE; } :visited { color: #551A8B; } :link:active, :visited:active { color: #FF0000; } :link, :visited { text-decoration: underline; cursor: pointer; }

Those defaults hold a lesson about color alone. The blue, #0000EE, has a contrast of 9.4:1 on white, and the purple, #551A8B, 11.0:1. But against black body text, the blue reaches only 2.2:1, below the 3:1 that WCAG's technique for color-only links asks for, and the visited purple and unvisited blue differ from each other by just 1.17:1. A default link is told from the text around it by its underline, not its color. The convention works because it was never color alone. Links people can use covers the rule, and the privacy limits on what :visited may change; page 08, Color for people, makes "never color alone" a rule for every state.

Names in the browsers, sixteen in the standards, then 147

Names reached authors before any standard listed them. Internet Explorer 2.0, in late 1995, let bgcolor "be set to a colorname", such as red or fuchsia, and by late 1996 Internet Explorer 3 and Netscape Navigator accepted about 140: mostly names from the X Window System, the Unix graphics system the first browsers were built on, plus the sixteen VGA colors with their VGA values.

When the standards arrived, they took only those sixteen. CSS1, a W3C Recommendation in December 1996, suggested sixteen color keywords and said plainly where they came from: "These 16 colors are taken from the Windows VGA palette." HTML 3.2 standardized the same sixteen in January 1997: black, silver, gray, white, maroon, red, purple, fuchsia, green, lime, olive, yellow, navy, blue, teal and aqua. So green is #008000, and the pure green most people expect is called lime. CSS1 also gave hex a three-digit shorthand, #rgb, and a decimal form, rgb(51, 102, 153).

SVG 1.0 standardized the browsers' whole list in 2001, and CSS Color 3, a Recommendation in June 2011, adopted it, adding 131 names to reach 147. That list grew over years rather than being designed, which is why it contains papayawhip, and why darkgray (#a9a9a9) is lighter than gray (#808080): the list took gray from the VGA palette and darkgray from X11, whose own gray was #bebebe. CSS Color 3 also brought hsl(), alpha through rgba() and hsla(), and made transparent a true color.

The last name came in June 2014. Rebecca Meyer, daughter of the CSS author Eric Meyer, died on her sixth birthday, 7 June 2014. The web community proposed a named color in her memory, beccapurple, for #663399. Meyer set one condition: that it be called rebeccapurple, because a couple of weeks before she died, she had told her parents that she was about to be a big girl of six, and "Becca was a baby name". The CSS Working Group agreed on 21 June; it is the 148th named color in CSS Color 4. It is also, by its hex, a web-safe color.

bgcolor="chucknorris"

CSS throws away a color it cannot parse; the declaration is ignored. HTML's old color attributes, bgcolor, text, color on <font> and the rest, were never that strict. Early browsers repaired whatever authors typed, pages came to depend on it, and the HTML standard now defines the repairs exactly, as the "rules for parsing a legacy color value". Almost every string becomes some color; only an empty value and transparent are rejected. That is how a joke on a Stack Overflow question, "Why does HTML think 'chucknorris' is a color?", became a fixture of web trivia.

How the legacy color parser turns "chucknorris" into #c00000Five steps, top to bottom. One: the attribute value chucknorris, eleven characters. Two: every character that is not a hex digit becomes zero, giving c00c0000000; the c's survive because c is a hex digit. Three: zeros are added at the end until the length is a multiple of three, giving twelve characters, c00c00000000. Four: the string is split into three equal parts of four characters, c00c, 0000 and 0000, one each for red, green and blue. Five: each part is cut to its first two characters, c0, 00 and 00, giving the color #c00000, a dark red. A swatch of that red is labeled #c00000.1. the attributechucknorris2. non-hex → 0c00c00000003. pad to a multiple of 3c00c000000004. split in threec00c00000000redgreenblue5. keep two digits each#c0 00 00#c00000An invalid color in CSSis ignored. In an HTMLattribute, it is repaired.
The HTML standard's "rules for parsing a legacy color value", applied to bgcolor="chucknorris". Chrome 156 draws the cell rgb(192, 0, 0).
<!-- Obsolete attributes. Browsers still repair whatever they are given. --> <table> <tr> <td bgcolor="chucknorris">dark red: #c00000</td> <td bgcolor="369">near black: #030609, not #336699</td> </tr> </table>

The second cell is the version of the bug that bit real pages. Without its #, 369 is not read as three-digit shorthand; the parser splits it into one digit per channel and gives a color one notch above black. The standard keeps the shorthand rule only for exactly four characters starting with #. Both attributes are obsolete, and CSS's strictness is the better design: an invalid color in a style sheet fails visibly, and the cascade falls back to the previous declaration, so a fallback written first still works.

From sRGB to oklch()

Through the 2000s every CSS color was an sRGB color, and since nearly every screen was an sRGB screen, nobody needed more. That changed when phones and laptops gained wide-gamut panels. In 2016, Dean Jackson of Apple's WebKit team proposed a color() function for P3 colors; by 2020 WebKit's blog was explaining color(display-p3 …) as shipping in Safari.

Meanwhile Lea Verou, a co-editor of CSS Color 5 and later of CSS Color 4, argued in 2020 that CSS needed a perceptual notation, and that lch(), already in the draft, was the one to use. Ottosson's Oklab, published that December, fixed the blue-to-purple drift that made lch() awkward, and the specification adopted oklab() and oklch() alongside the CIE functions. On 9 May 2023, Firefox 113 shipped the last missing pieces, and the new color functions were in every major engine: Baseline, and widely available from November 2025. The table above gives later dates; Color in CSS covers the notations, Deriving color relative color, contrast-color() and alpha(), and Light and dark light-dark().

9. The web-safe palette

For a few years in the late 1990s, careful web designers chose every flat color from a list of 216. The list is obsolete now, but the problem it solved, the gap between the color you ask for and the color the screen can show, is the subject of this whole section, and the techniques it involved come back in GIF images and in gamut mapping.

Why it existed

Many computers of the mid-1990s ran their displays in 8-bit color. Each pixel was stored as one byte, an index into a table, the palette, of 256 colors, and only those 256 could be on the screen at once. The operating system kept some for itself: Windows "reserves 20 colors", as Microsoft's Robert Hess wrote in 1996, for drawing windows and menus, and the Mac and Unix workstations each managed their own palettes differently, so the same 256 entries held different colors on each system.

A browser had to fit every page into the colors it had left. It did that with its own fixed palette, and anything off that palette was either snapped to the nearest color the browser had or dithered: drawn as a pattern of pixels in nearby colors, which the eye blends at a distance. Dithering worked tolerably on photographs and badly on flat areas. Lynda Weinman, co-founder of lynda.com, put it bluntly: "When a browser dithers flat colors it looks far more objectionable than when it dithers photographs."

Why 216

The fixed palette the browsers used (Netscape's, and nearly the same one in Internet Explorer, whose Windows palette held 213 of the 216) was a color cube whose colors were available on both Mac and Windows: six evenly spaced levels of each channel. Six levels make five equal steps, and 255 ÷ 5 = 51, so the levels are 0, 20, 40, 60, 80 and 100 percent of full intensity: 0, 51, 102, 153, 204 and 255, or in hex 00, 33, 66, 99, CC and FF. Six levels in three channels give 6 × 6 × 6 = 216 colors; six is the largest number whose cube fits in 256, which leaves 40 entries for the system. Weinman wrote that the remaining 40 "vary on Macs and PCs", so only the 216 could be counted on.

That cube is why web-safe hex values look the way they do, with each channel a doubled digit from the set 0, 3, 6, 9, C and F: #336699, #CC0000, #FFCC00. It is also why the three-digit hex shorthand of CSS1, in which #369 means #336699, felt so natural at the time. Every web-safe color has a three-digit form, and designers wrote them that way.

Who popularized it

The palette came from the browsers; its name and its fame came from designers. Weinman published it in 1996, in Designing Web Graphics and on her site, as "The Browser Safe Color Palette", a name she noted was hers. She credited Bruce Heavin with working out the colors by comparing a Mac and a PC, and Joy Silverman with 60 hours arranging it.

Its end

By 2000 most computers ran in 16-bit or 24-bit color. A 24-bit display can show every one of the 16.7 million colors that six hex digits name, so for it the palette was unnecessary. A 16-bit display, the commonest kind in 2000, was different. It keeps 5 bits for red and blue and 6 for green, 32 and 64 levels, and apart from black and white none of those levels lands exactly on a web-safe value. So every other web-safe color had to be shifted, and browsers could shift a GIF and a bgcolor of the same color to different values. The palette meant to prevent mismatches now caused them.

On 6 September 2000, David Lehn and Hadley Stern, designers at Razorfish, published "Death of the Websafe Color Palette?" in the magazine Webmonkey. They placed a GIF of each of the 216 colors inside a table cell whose bgcolor had the same hex value, and looked for a visible seam. They tested Internet Explorer and Netscape on Windows and the Mac, at 8, 16 and 24 bits. Even at 8 bits, Internet Explorer 4 and 5 on Windows shifted four of the 216, two greens and two blues. At 16 bits the results were far worse: "only 22 of the 216 colors we began with did not end up being shifted incorrectly in at least one viewing environment". They called those "roughly 22 colors" the "Reallysafe Palette". Most of the survivors were greens; the authors titled the list "The Reallysafe Palette (hope you like green ...)".

By then 8-bit displays were disappearing, and nobody adopted a palette of 22 colors. Designers went back to choosing colors for what they looked like. The 216 live on as a ready-made grid in color pickers; Photoshop's still offers an "Only Web Colors" checkbox. As a grid to choose from, its problem is no longer reproduction but too much choice: 216 swatches with no roles. Choosing and pairing starts from a few roles instead.

What it still teaches

  • A palette can be a constraint of the medium. The 216 were not chosen for beauty; they were what the hardware could show reliably. Today's constraints differ (dark mode, forced colors, projectors, print), but designing within the medium is the same job.
  • Quantization and dithering. Reducing a photograph to a fixed set of colors, and hiding the steps by scattering pixels, is exactly what a GIF encoder does when it fits a photo into 256 colors. Images, Raster formats shows the cost in bytes. Page 07, Displays and environments, meets the same problem again as gamut mapping: a color the screen cannot show must be replaced by one it can.
  • The color you ask for is not the color the screen shows. In 1997 the gap came from 256-entry palettes; now it comes from gamuts, calibration, brightness, the room and the reader's eyes. WCAG still carries a trace of the old problem: its definition of relative luminance notes that "if dithering occurs after delivery, then the source color value is used". Contrast is measured on the color you specified, and everything this section teaches about tokens is about keeping that specification meaningful.

10. Further reading

11. Summary

How color works, in brief
ConceptKey points
Three conesVision reduces a spectrum to three signals, so three numbers describe a color. Different spectra can look identical (metamerism), which is how a screen makes yellow from red and green.
Additive and subtractiveScreens add red, green and blue light to black; print subtracts with cyan, magenta, yellow and black ink from white. The gamuts overlap but differ.
Model and spaceA model (RGB, HSL, HWB) arranges numbers; a space (sRGB, Display P3, Rec. 2020) fixes primaries, a white point and a transfer function. Hex, names, rgb(), hsl() and hwb() always mean sRGB.
GamutThe colors a space or screen can show: a triangle on the chromaticity diagram. P3 adds mostly greens and reds to sRGB; Rec. 2020 reaches almost to the edge of vision.
Encoded and linearsRGB numbers are encoded: #808080 sends 21.6% of white's light. Averaging encoded values darkens midpoints; linear light is right for physics, not for looks.
Perceptual spacesCIELAB (1976) and Oklab (2020) aim for equal steps that look equal; LCH and OKLCH are their polar forms. OKLCH's L holds steady across hues; HSL's does not.
Luminance and lightnessLuminance is physical, from linear light, and WCAG contrast is built on it. Lightness is perceptual, roughly its cube root. Related, not interchangeable: compute contrast from luminance.
Link colorsBlue and purple since Mosaic 0.13 in 1993; #0000EE and #551A8B in the HTML standard today. Against black text the blue is only 2.2:1, so the underline does the work.
Names and parsingHex first (Netscape 1.1, 1995); names from late 1995, about 140 by 1996; sixteen standardized (1996–97); all 147 in SVG 1.0 (2001) and CSS Color 3 (2011); rebeccapurple (2014). HTML's legacy parser repairs almost any string: chucknorris is #c00000. CSS rejects invalid colors instead.
Web-safe palette216 colors, six levels per channel, for 8-bit displays that dithered everything else. Named "browser-safe" by Lynda Weinman in 1996; shown in 2000 to be reliable for only about 22 colors on 16-bit displays.
What it still teachesPalettes as constraints of the medium; quantization and dithering (GIF, gamut mapping); the gap between the color you specify and the color the screen shows.