Skip to content
ColorTokensKitby Penguin Design Ventures
Contents
Explanation

What does equal lightness cost?

What equal contrast costs, from quieter reds to olive yellows, and what you can do about each.

Updated View as Markdown
Try it / Every hue at one stop
View
Stop · drag across_400
_50_1000

L* 67.5 in every hue · chroma cap 0.161 · proTurquoise, proCyan, proCobalt, proIndigo, proIris, proViolet can't reach it

0.00.10.20.3the palette's chroma at _400pinkrubyredtomatocoralorangebrownambergoldmustardyellowolivechartreuselimegrassgreenjadeemeraldminttealturquoisecyanceruleanskyazurebluecobaltindigoirisvioletgrapepurpleorchidplummagentaroseThe palette at _400: one lightness, one chroma, every hue

Colored area: the most chroma Display P3 can show at this lightness, hue by hue. Faded above the line: color we leave out so every hue is equally vivid. Black marks under the line: hues that can't reach the cap, which keep 98% of their maximum.

The faded color above the line is what the palette leaves out, so that every hue at a stop is equally vivid and has the same contrast. At _400, plum, magenta and the greens give up the most. Pick _600, the first stop that passes WCAG AA for text on white, and the purples, pinks and reds use less than half the chroma Display P3 allows them.

Why every hue shares one chroma#

#fff6c0
Rejected yellow _50
#eef6ff
Rejected blue _50
#0e006b
Rejected indigo _1000
#00220a
Rejected green _1000

These stops come from a palette we tried and rejected, which gave each hue the most chroma sRGB allows at each stop's L*. Contrast stayed equal, but vividness didn't. At _50, yellow got 3.5 times the median chroma, so it looked far more colorful than the rest. At _1000, indigo, iris and violet got more than twice the median, and in blues extra chroma also reads as brighter. That's the Helmholtz–Kohlrausch effect, strong in blues and negligible in yellows.

So each stop keeps one chroma for every hue, and the palette gets more vivid only through Display P3's wider range.

Quieter than other palettes#

#ef8383
proRed._400
#fb2c36
Tailwind red-500

Both palettes go past sRGB, but Tailwind lets each hue pick its own lightness and chroma, so its colors are more vivid. Ours hold one lightness and one chroma per stop, so contrast is the same in every hue. When one color should stand out, saturate(by: 1) gives it nearly the most chroma Display P3 allows, at the same contrast:

swift
Color.proRed._600.toColor().saturate(by: 1)   // twice the chroma, still 4.68:1 on white

The result leaves the palette, which is fine for an accent.

Less color on sRGB screens#

The palette is Display P3, with no sRGB fallback, like Tailwind's. On a screen that only shows sRGB, such as many external monitors, the system clips the 301 stops beyond sRGB, so they look less vivid there and their contrast can shift a little. sRGB-only screens shows how much.

Yellow turns olive at the darker stops#

#837509
proYellow._600
#956d09
proGold._600
#ded59e
proYellow._200

Readable yellow text on white needs _600, as dark as blue _600, and a dark yellow reads as olive. For a darker, warmer yellow, use proGold. For a yellow that reads as yellow, use a light stop such as _200.

Dark stops keep a little hue#

#0b1c32
proBlue._1000
#000001
proGray._1000

A family's backgroundPrimary uses _1000 in dark mode, which keeps a little chroma, so a blue-themed screen is navy, not black. When you want black, use the app-wide Color.backgroundPrimary from ColorTokens.swift.

Telling neighboring hues apart#

#fbc4d5
proPink._200
#fdc4cb
proRuby._200
#fec5c3
proRed._200
#fdc6ba
proTomato._200

Every hue sits 10° from its neighbors, so the light stops of pink, ruby, red and tomato are hard to tell apart. At any one stop, two families also differ only in hue, which some people with a color vision deficiency can't distinguish. When colors carry meaning, pick families further apart on the wheel, as harmonies do, and add a label, an icon or a difference in lightness, as WCAG asks. Using tokens in charts covers this for data.

Other contrast models#

WCAG 2 contrast is identical across hues at each stop, because it depends only on luminance. APCA, a newer model proposed for WCAG 3, varies a little by hue: it rates _700 text on _100 between 69.5 and 70.3.

What it isn't designed for#

ColorTokensKit is built for app interfaces in SwiftUI. It needs extra work, or isn't a good fit, when:

  • You must match an exact brand hex. ProTheme(hex: "#00B386") keeps #00B386 as the family's own color, but its stops follow the palette's lightness and chroma, and #00B386 isn't one of them. Replacing your app colors shows how to build that family.
  • You need HDR color, or a fallback designed for sRGB screens. The palette is Display P3 only.
  • You're choosing colors for print, photos or illustrations. The palette is tuned for readable screens.
  • Your app is built mostly in UIKit or AppKit. The functions return SwiftUI Color, and UIKit and AppKit show how to convert colors that adapt to dark mode.

Sources#

See also