Skip to content
ColorTokensKitby Penguin Design Ventures
Contents
Function

Stops: ._50 to ._1000

Twenty fixed colors per theme, lightest to darkest, with the same contrast in every hue.

Updated View as Markdown

Every theme has 20 stops, from _50, nearly white, to _1000, nearly black. A stop is one fixed color, and it has the same lightness in every hue, so it has the same contrast too. allStops gives you all 20, lightest first.

swift
let theme = Color.proBlue

theme._50        // the lightest stop
theme._600       // as light as every other hue's _600
theme._1000      // the darkest
theme.allStops   // all 20, _50 first

Here are proBlue's 20:

50
100
150
200
250
300
350
400
450
500
550
600
650
700
750
800
850
900
950
1000
Color.proBlue, OKLCH hue 250°

Making a token from two stops#

A stop stays the same in dark mode, so views draw with tokens, and each token picks one stop for light mode and one for dark. When you need a color the 20 tokens don't cover, name its two stops:

swift
extension ProTheme {
    var badgeBackground: Color {
        Color(light: _200.toColor(), dark: _800.toColor())
    }
}

Text("New")
    .padding(8)
    .foregroundStyle(Color.proGreen.foregroundPrimary)
    .background(Color.proGreen.badgeBackground)
light mode
New

Color(light:dark:) explains the switch, and understanding semantic tokens lists the stops behind the 20 built-in tokens.

The same contrast in every hue#

Two stops have the same contrast in every hue, so a pair you check once works in every theme:

swift
Color.proRed._800.contrastRatio(to: Color.proRed._100)     // 7.91
Color.proTeal._800.contrastRatio(to: Color.proTeal._100)   // 7.91, the same

proGray is the exception. Its stops sit on a lightness ladder of their own, so check gray pairs on their own.

Stepping through the ladder#

allStops is in order, so a stop's index is its number divided by 50, minus 1: allStops[11] is _600. That makes a light-to-dark scale for a heat map or a chart easy to slice:

swift
let levels = Array(Color.proBlue.allStops[3...15])   // _200 to _800, light to dark

How it behaves#

Every hue shares one lightness per stop, from _50 to _1000 in steps of about 4 to 5 L*, and one chroma per stop, most vivid around _350. How the ramps are built has the numbers.

Each stop is itself a ProTheme, whose own color is that stop. Its stops and tokens read the same ramp, so Color.proBlue._600._200 is Color.proBlue._200, and Color.proBlue._600.foregroundPrimary is Color.proBlue.foregroundPrimary. A theme from ProTheme(hex:) works the same way: its stops come from its hue's ramp, not from your hex.

Many stops are beyond sRGB, in Display P3. getHexString() clamps those into sRGB, so name a stop in code instead of copying its hex.

API#

swift
public extension ProTheme {
    var _50: ProTheme { get }
    var _100: ProTheme { get }
    // _150 to _950, every 50
    var _1000: ProTheme { get }
    var allStops: [ProTheme] { get }
}

Each stop returns a ProTheme that holds that stop's fixed color and reads the same ramp. allStops returns all 20, from _50 to _1000.

Sources#

See also

  • Understanding semantic tokens Learn the 20 tokens by the job each one does, and get dark mode and passing contrast with them.
  • Color(light:dark:) Make any color switch between light and dark mode, the way every token does.
  • Ready-made themes 37 themes to use as they are, from proPink to proRose, each with 20 stops and every token.
  • How the ramps are built The lightness and chroma behind every stop, so any hue you pick gives the same contrast.
  • .lighten(by:) Add highlights and lighter steps to any color, landing on an exact palette stop.