Two red squares:
leftrightIn hex, the only notation most of us have for colour, both are #ff0000. The left one because that’s what it is. The right one because hex has no other number for it.
If you’re reading this on a MacBook, an iPhone, or most monitors sold in the last few years, the right one is redder. It’s a purer red, and once you’ve seen the two side by side it’s obvious, but there’s no way to name it with the tools you use every day.
On an older monitor they’re the same red, and that’s part of the story too.
So the squares raise a question about #ff0000 itself. Two things that both call themselves #ff0000 can’t both be the reddest red, so what does #ff0000 mean?
Let’s start with what a colour value is, because most of us have been using them for years without ever being told.
#ff0000 is three numbers: 255 of red, 0 of green, 0 of blue. Three numbers can describe a colour, but only once you’ve said three more things: which red, which green, and which blue. “255 of red” means “as much of this particular red light as the screen has”, and different screens have different red lights. A number without a light attached is like a temperature without a scale.
The picture that makes this obvious was drawn in 1931, and almost nobody shows it to web developers. That year the International Commission on Illumination, the CIE, published a map of every colour a human can see, laid flat with brightness left out. They built it from experiments in which a handful of people (ten in one lab, seven in another) looked at a small split field and turned three coloured lights up and down until one half matched a pure wavelength on the other. Plot the results and you get a horseshoe:
The curved edge is the spectrum, one wavelength at a time: blue at the bottom left, green at the top, red at the right-hand corner. The straight edge along the bottom is the purples, which no single wavelength produces; you only get them by mixing red and blue light. Everything inside the shape is a colour a person can see.
Now pick three lights, a red, a green and a blue. Put a dot on the map for each and join them up, and you get a triangle like the two drawn here. That triangle is every colour those three lights can mix. Anything outside it is a colour you can see but a screen built from those lights can’t show you, and since the horseshoe bulges outward, no triangle of real lights covers all of it.
A screen is three lights, so the colours it can show are a triangle on this map, called its gamut. A colour value picks a point in that triangle by saying how much of each corner to use, and #ff0000, all red and nothing else, is the red corner. Which means #ff0000 is a different colour on every screen with a different red light, unless somebody writes down which triangle they mean.
Somebody did, in 1996.
In November 1996 four engineers, two from Hewlett-Packard and two from Microsoft, published a proposal with a plain title, “A Standard Default Color Space for the Internet”, and a plain aim: a colour definition “based on the average performance of personal computer displays”. They called it sRGB, and it pinned down three things: the triangle, the curve that turns a number like 128 into an amount of light, and the room you were assumed to be sitting in. A triangle with those details attached is what’s called a colour space.
The triangle came from television. The authors took it from Rec. 709, the HDTV standard agreed six years earlier, and its three corners described the phosphors glowing inside the cathode-ray tubes of the day. Computer monitors were the same kind of tube with the same phosphors, so the web’s red became a 1990 TV tube’s red.
The web took it up fast. HTML 4.0 adopted it in December 1997 and CSS2 in May 1998, with one sentence: “All RGB colors are specified in the sRGB color space.” That sentence stayed through CSS Color 3, and the current spec still says the same thing in other words. So every hex colour since 1998 has meant a point in the 1996 triangle.
Put CSS2’s sentence next to the horseshoe. It doesn’t say “the screen’s red”, it says sRGB’s red, and that’s a promise that cuts both ways. The browser promises that #ff0000 will look like the 1996 red on every screen it can, which is why a colour you pick on one machine looks the same on another. In exchange, the screen’s own red is out of reach, because ff is as much red as hex can ask for and the promise pins it to the 1996 corner. In 1996 that cost nothing. Every screen was more or less that triangle, so the screen’s red and sRGB’s red were the same light, and the promise stayed free for as long as monitors stayed the same. They didn’t.
The next triangle came from cinemas. In July 2005 the studios’ Digital Cinema Initiatives published a specification for digital projectors, and its triangle, wider than sRGB’s in red and green, became known as P3. It sat in projection booths for a decade.
Then Apple put it in a desk. In October 2015 the iMac with Retina 5K display shipped with, in Apple’s words, “a wider P3-based color gamut that provides a 25 percent larger color space”. The 9.7-inch iPad Pro followed in March 2016, and the iPhone 7 in September 2016. The version Apple shipped isn’t quite the cinema one: it keeps P3’s corners but uses the same white and the same curve as sRGB, so only the corners move and everything else about an sRGB colour carries over. That’s why CSS calls it display-p3: the working group started with dci-p3 and renamed it in 2016, because the two aren’t the same.
So by the end of 2016 there were screens in a great many pockets and on a great many desks whose red corner was outside the 1996 triangle, and the browser, keeping its promise, showed them the 1996 red anyway.
The first browser to do something about it was Safari. In July 2016 Dean Jackson wrote on the WebKit blog (WebKit is Safari’s engine) that HTML and CSS had only ever been defined to work in sRGB, and put a red square on the page with a faint WebKit logo hidden in it. The square was a P3 image filled with P3’s full red, and the logo was drawn in a slightly less full red. On an sRGB display both reds are outside the triangle and get pulled back to the same corner, so the logo vanishes. On a P3 display you can see it. It was these two squares, ten years earlier, done with a picture because CSS had no way to say it yet.
The way to say it was already being drafted: a function called color() that names the triangle first and then gives the three numbers, on a scale of 0 to 1. It appeared in the first public draft of the CSS colour spec that same week. Safari 10.1 shipped color(display-p3 1 0 0) in March 2017 and Chrome 111 in March 2023. Firefox 113 accepted the syntax in May 2023, but it still turns the colour into sRGB before drawing it. For six years Safari was the only browser that could reach the screen’s red, and WebKit was still saying so in January 2020.
On the same map, P3’s red corner sits just outside the 1996 one:
Your browser sends a P3 screen different numbers for the two squares, and you can see them live:
#ff0000color(display-p3 1 0 0)The tag in the top corner is the color-gamut media query, which reports roughly whether your screen covers the P3 triangle (Chrome says yes once it covers 90 percent). The two numbers underneath don’t depend on your screen at all. The page draws each colour into a canvas that works in P3 and reads the pixel back, which gives the values the browser would send a P3 panel, whatever panel you actually have. On a P3 screen the squares look different. On an sRGB screen they don’t, because the P3 red has nowhere to go but the corner, and the browser puts it there.
If your browser can do the measurement, hex red comes back as (234, 51, 35), not (255, 0, 0).
A P3 panel has a red light, a green light and a blue light, and its own scale of 0 to 255 for each. The browser has promised that #ff0000 means the 1996 red, and on the P3 map the 1996 red isn’t at the corner, it’s a little way inside. So to show it, the browser turns the panel’s red most of the way up and adds a little green and blue to pull the colour in from the corner. The spec gives the arithmetic, from the 1996 triangle to the P3 one by way of the CIE’s map, and it turns sRGB’s (255, 0, 0) into P3’s (234, 51, 35): 234 of the 255 steps the panel’s red has, 51 of its green, 35 of its blue.
I did the sum with the matrices in the CSS specification and got 0.917, 0.200 and 0.139, which out of 255 is 234, 51 and 35. I did it again with the constants in Chrome’s graphics library, Skia, and got the same to two decimals (the two use slightly different constants and part company in the third). And a screenshot of the page, taken with Chrome forced to a P3 output, reads (234, 51, 35).
Now go the other way. Take P3’s red corner, (255, 0, 0) in P3, and write it on sRGB’s scale, and it comes out at 109 percent red, minus 23 percent green and minus 15 percent blue. A negative amount of a light. That’s what “out of gamut” means when you write it down: the colour is real, but making it from sRGB’s corners needs more red than sRGB has and less green than none. Hex can’t write that, and neither can anything else in the 1996 system.
So what does a browser do when it’s handed a colour it can’t show? The obvious thing is to clip it: any channel over 255 becomes 255, and any channel under 0 becomes 0. CSS Color 4 calls that “the simplest and least acceptable method” and describes three better ones that walk the colour inward while keeping its hue and lightness. No browser runs any of them. Chrome’s engineers said in 2022 that the section would probably have to be dropped for performance, WebKit deleted its unfinished version in November 2025 with the note “For now, we always clip”, and Firefox has one behind a setting that defaults to Clip. So every engine clips, and P3 red on an sRGB screen becomes (255, 0, 0). That’s why the two squares are identical on that screen: the browser can tell them apart, but it throws the difference away at the last step.
Where that last step happens matters. Chrome doesn’t clip when it reads your CSS. It keeps the colour as three floating-point numbers, negatives and all, and hands them to Skia, which converts them into the colour space it’s drawing in and only then clips. On a Mac with the GPU doing the drawing, that’s the display’s own colour space, not sRGB: Chrome reads the monitor’s colour profile (the file that says which triangle the screen has) and draws in it. A comment in Chromium’s source gives the reason: matching the display exactly saves about half a watt when redrawing the full screen 60 times a second. So the clip happens in P3 on a P3 screen and in sRGB on an sRGB one, which is why the same colour survives on one and not the other.
Firefox is different. It understands color(display-p3 1 0 0), but its style code turns every CSS colour into an ordinary 8-bit sRGB value before anything is painted. So in Firefox, even on a P3 screen, the right square is the left square, and the bug asking for that to change is still open.
The promise also shows up somewhere people notice without knowing why: screenshots that look duller once they’ve been sent somewhere.
Say you screenshot the right-hand square on a P3 screen. The file macOS saves carries the display’s colour profile inside it (I checked one; Apple doesn’t document this anywhere I could find), so any app that reads profiles shows the square the right red. But some tools drop the profile when they pass an image along, and an image with no profile gets treated as sRGB, in Chrome and Safari, by the same 1996 rule that governs CSS. (Firefox, by default, only converts images that have a profile and sends the rest straight to the panel.) So the pixel that meant P3’s (255, 0, 0) is now read as sRGB’s (255, 0, 0), a tamer red. Nobody changed a pixel; the file just lost the note that said which triangle its numbers belonged to.
The same rule applies to images you make yourself, and I measured that too. Give Chrome a PNG with no colour information or a plain <canvas>, and it treats them as sRGB and converts them for the panel: on a P3 output, a pure red pixel in either comes out as (234, 51, 35), the same as #ff0000. Tag the PNG with a P3 profile, or create the canvas with colorSpace: "display-p3", and the red survives as (255, 0, 0). So in Chrome and Safari, which both read the profile on an image, a PNG tagged as P3 can show the panel’s red even in a browser without color(), and a screenshot that loses its profile loses the red with it.
Go back to the two squares.
The left one is the promise. #ff0000 is the red corner of a triangle four engineers drew in 1996 around the phosphors of a television tube, and every browser on every screen since has kept that promise, converting the number into whatever the panel needs, (234, 51, 35) on a Display P3 panel, so that what you see is the 1996 red and not the screen’s. The right one is the screen’s own red. It’s been there since 2015 on a Mac, out of reach from hex, reachable in Safari since 2017 and in Chrome since 2023 and not yet in Firefox, and you get it with a function that says which triangle you mean.
None of that is a mistake. The promise is why the web’s colours have stayed put through every kind of screen since 1998. The cost is that it was made about one monitor, and that monitor went away.
So the reddest red on your screen isn’t #ff0000, which is a point on a map from 1996. The map was good enough that nobody looked at its edge for twenty years, until the screens grew past it and the number stayed where it was.