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.
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 firstHere are proBlue's 20:
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:
extension ProTheme {
var badgeBackground: Color {
Color(light: _200.toColor(), dark: _800.toColor())
}
}
Text("New")
.padding(8)
.foregroundStyle(Color.proGreen.foregroundPrimary)
.background(Color.proGreen.badgeBackground)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:
Color.proRed._800.contrastRatio(to: Color.proRed._100) // 7.91
Color.proTeal._800.contrastRatio(to: Color.proTeal._100) // 7.91, the sameproGray 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:
let levels = Array(Color.proBlue.allStops[3...15]) // _200 to _800, light to darkHow 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#
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#
- The library's ProTheme.swift, which defines the stops, and UniformRamp.swift, which sets each stop's lightness and chroma.
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.