Skip to content
ColorTokensKitby Penguin Design Ventures
Contents
Explanation

How the ramps are built

The lightness and chroma behind every stop, so any hue you pick gives the same contrast.

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.

Each square is a stop of one hue's ramp, with lightness up and chroma to the right. The colored area is every color Display P3 can show at that hue. Drag the hue: every ramp follows one recipe, with one lightness and one chroma per stop, and a hollow square marks a chroma the hue can't reach.

So _600 is 4.68:1 on white in all 36 named hues and in any hue you pass, and no hue looks louder than another. How did we choose and build these colors? has the short version.

One lightness per stop#

Lightness is CIELab L*, which decides WCAG contrast, so a stop has the same contrast in every hue. It runs from 96.5 at _50 to 10.2 at _1000.

We didn't invent those values: each is the average, at that stop, of the library's earlier hand-tuned palette. Why OKLCH? explains why lightness comes from L* and not from OKLCH.

One chroma per stop#

Each stop's chroma is the most that at least 29 of the 36 named hues can show in Display P3 at that lightness, rounded down. It's highest from _350 to _450, because screens show their most vivid colors at medium lightness.

Why 29? Any higher, and more hues would fall short and look duller than their neighbors. Any lower, and every hue pays: at _400, a cap low enough for every hue would sit 7% lower, where indigo runs out.

A hue that can't reach it gets 98% of what Display P3 allows, at the same L*, so its contrast still matches. From _50 to _400, the violet-blues fall short, indigo the most. From _400 down, teal, turquoise and cyan fall short by a few percent, and mustard by less. Most other hues could show more color than they use, which what does equal lightness cost? covers.

Any hue, the same recipe#

swift
let blue = ProTheme.primary(forHue: 250)   // the same ramp as Color.proBlue
let brand = ProTheme(hex: "#00B386")       // the ramp at your hex's hue, 167°

The named families are just hues, so any hue you pass, or your brand's, gets the same recipe and the same contrast. In the library, each stop takes four lines of UniformRamp.swift:

swift
let luminance = Gamut.luminance(lightnessStar: lightnessStar)
let chroma = min(targetChroma, gamutMargin * Gamut.maxChroma(luminance: luminance, hue: hue))
let oklabLightness = Gamut.lightness(forLuminance: luminance, chroma: chroma, hue: hue)
return OKLCHColor(l: oklabLightness, c: chroma, h: hue)

Gray's own ladder#

#626262
proGray._600
#4176b8
proBlue._600

Gray has no hue, so its stops sit on a hand-tuned ladder of their own. Gray _600 is 6.09:1 on white, where every hue's _600 is 4.68:1, so check gray pairs on their own.

Sources#

See also

  • .contrastRatio(to:method:) Check WCAG 2 and APCA contrast in Swift, and keep your color pairs passing with a unit test.
  • .lighten(by:) Add highlights and lighter steps to any color, landing on an exact palette stop.
  • Fitting colors into Display P3 Stay inside Display P3 without losing contrast: chroma drops, lightness holds.
  • What does the library offer? 36 color themes, ready-to-use tokens with dark mode, a theme for your brand, and state functions.
  • .toLCH() Read a color as CIELab lightness, chroma and hue, to match LCH numbers or check contrast.