Skip to content
ColorTokensKitby Penguin Design Ventures
Contents
Explanation

Fitting colors into Display P3

Stay inside Display P3 without losing contrast: chroma drops, lightness holds.

Updated View as Markdown
Try it / The Display P3 gamut at one hue
OKLCH chroma 00.1850.37

OKLCH lightness runs from black at the bottom to white at the top. The stops climb the gray axis on the left, reach out to the shared chroma in the middle, and pull back where the gamut narrows.

The colored area is every color Display P3 can show at hue 40°, coral, with lightness up and chroma to the right. It's widest at medium lightness and narrows toward white and black, so a vivid light coral has nowhere to go. Drag the hue to see where other hues run out.

swift
let coral = OKLCHColor(l: 0.85, c: 0.25, h: 40)   // beyond Display P3
coral.toColor()                                   // passed on as is, so the system clips it

Clipping a light coral into Display P3 makes it about two stops darker, and more orange. So when ColorTokensKit makes a color, it lowers chroma instead and keeps lightness, and with it contrast. A color you build yourself, like this one, is left as it is.

Tokens and color functions hold lightness#

swift
// Asks for chroma 0.300; Display P3 allows 0.236 at this lightness and hue.
Color.proGreen._500.toColor().saturate(by: 1)   // L* 58.2, 3.37:1 on white, as before

Ask saturate(by:) for more chroma than Display P3 can show, and you get the most it can, at the same contrast. The ramps and the other color functions fit colors the same way, and a palette color lands on an exact stop instead.

Blends and gradients follow CSS Color 4#

blend(with:by:) and every proGradient blend fit colors into Display P3 with the gamut mapping CSS Color 4 defines for browsers. It lowers chroma at the same OKLCH lightness, and stops once clipping would be within a just-noticeable difference. That keeps colors at the edge of the gamut vivid, and gradients free of kinks. L* can shift a little, which a gradient can afford because it doesn't set text contrast.

sRGB-only screens#

#0a837f
proTeal._600
#008580
Clipped to sRGB

The palette targets Display P3, the gamut of every iPhone since the iPhone 7, and 301 of its 720 colored stops lie beyond sRGB. There's no sRGB fallback. On a screen that only shows sRGB, such as many external monitors, the system clips those colors, so they look less vivid.

Clipping moves lightness a little too. The text tokens keep AA with room to spare, but a bare _600 from mint to cerulean can drop to 4.48:1 on white, just under AA.

Hex can't hold these colors either: getHexString() clamps a color beyond sRGB into it. This wiki writes palette colors as Display P3 hex.

Sources#

See also

  • What does equal lightness cost? What equal contrast costs, from quieter reds to olive yellows, and what you can do about each.
  • Why OKLCH? Every hue looks evenly matched at each stop, so you can swap families without rechecking contrast.
  • .toRGB() Read a color's red, green and blue channels, even past sRGB, to hand to other graphics code.
  • How did we choose and build these colors? Why every hue shares one lightness and one chroma at each stop, and how the 36 hues were named.