Tokens are how design teams keep an app's colors consistent across every screen, theme and mode. To see why they matter, start with how most apps handle colors today.
The problem with how most apps are built#
Most apps keep their colors in one file: the brand color as a constant, a few asset catalog colors with a light and a dark value, and SwiftUI's own colors for the rest.
extension Color {
static let brand = Color(hex: "#F27333") // from the brand guide
static let brandPressed = Color(hex: "#D9602A") // a darker orange, picked by eye
static let card = Color("Card") // asset catalog: #F4F2EF light, #1C1C1E dark
static let textMuted = Color("TextMuted") // asset catalog: #6B6B70 light, #A1A1A6 dark
}
struct TripCard: View {
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text("Lisbon")
.font(.headline)
.foregroundStyle(.primary) // SwiftUI's own: black, or white in dark mode
Spacer()
Text("€620")
.foregroundStyle(Color.brand)
}
Text("3 nights from October 12")
.font(.subheadline)
.foregroundStyle(Color.textMuted)
Text("Book now")
.padding(8)
.foregroundStyle(.white)
.background(Color.brand)
}
.padding()
.background(Color.card)
}
}This works. The card follows dark mode through the asset catalog, the brand color lives in one place, and SwiftUI fills in the rest. The problems show up as the app grows:
- Nothing checks the colors. White on the brand orange measures 2.89:1, and the orange price on the light card 2.58:1, both well under the 4.5:1 that WCAG AA asks for text. Every pair is a guess until someone measures it.
- Dark mode by hand. Each asset needs a dark value someone picks and checks, and the brand orange and its pressed shade stay the same in both modes.
- Shades picked by eye.
brandPressedis a guess, and a disabled button, a hover state or a tag tint is another hex value each time. - Hard to scale. A second theme or a new feature color means another set of assets and another round of checks.
Even carefully picked colors don't line up. A color picker calls these four equally light, yet as text on white one vanishes and another is nearly black:
How tokens fix it#
Tokens fix it in two steps: every hue gets the same lightness at each stop, and each job, like secondary text, gets a name that picks the right stop in each mode. The rest of this page follows that path, from a hue to your app.
One hue becomes 20 stops#
We turn each family's hue into 20 stops, from near-white _50 to near-black _1000. A stop is one fixed color: Color.proOrange._600 is the same orange in both modes. All 36 hues share one lightness at each stop, so a stop you check once is checked in every hue.
A token names a job#
Your views don't need "orange _800". They need secondary text, a card background, a border. A token names that job and picks a stop for each mode: foregroundSecondary picks _800 in light mode and _200 in dark, the same distance from each end of the ramp:
Text("3 nights from October 12")
.font(.subheadline)
.foregroundStyle(Color.proOrange.foregroundSecondary) // _800 in light mode, _200 in darkIn ColorTokens.swift, the file of tokens you copy into your app, the token is those two stops and nothing more. Defining a token is where toColor() belongs: it turns a stop into the SwiftUI Color that Color(light:dark:) takes.
public extension ProTheme {
var foregroundSecondary: Color {
Color(light: _800.toColor(), dark: _200.toColor())
}
}We chose each token's stops so every text token passes WCAG AA on the page background, backgroundPrimary, in both modes and every hue.
A token for every part of a view#
We give you 20 tokens that cover a whole screen: foreground for text and icons, background for screens and cards, surface for see-through layers, outline for borders, and inverted versions for areas in the opposite appearance. Here's one of each:
let theme = Color.proOrange
VStack(alignment: .leading, spacing: 8) {
Text("Lisbon")
.font(.headline)
.foregroundStyle(theme.foregroundPrimary) // foreground: text and icons
Text("Flight included")
.font(.subheadline)
.padding(4)
.foregroundStyle(theme.foregroundSecondary)
.background(theme.surfacePrimary) // surface: see-through layers
.border(theme.outlineSecondary) // outline: borders
Text("Book now")
.padding(8)
.foregroundStyle(theme.invertedForegroundPrimary) // inverted: the opposite appearance
.background(theme.invertedBackgroundPrimary)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(theme.backgroundPrimary) // background: screens and cardsMost groups have a primary, secondary and tertiary token. Understanding semantic tokens lists them all.
Theme stops, theme tokens and app tokens#
Every ProTheme, like Color.proOrange, has 20 stops and all 20 tokens. There's also one more set of tokens for the app as a whole:
| Example | In dark mode | |
|---|---|---|
| Theme stop | Color.proOrange._800 | Stays _800 |
| Theme token | Color.proOrange.foregroundSecondary | Switches to orange _200 |
| App token | Color.foregroundSecondary | Switches to gray _200 |
Theme tokens let a view change theme in one line, as setting up themes shows. App tokens are the same 20 names on Color itself, built on gray, from ColorTokens.swift. Use them for the parts of your app that don't belong to a theme, such as settings:
HStack {
Text("Price alerts")
.foregroundStyle(Color.foregroundPrimary)
Spacer()
Text("On")
.font(.subheadline)
.foregroundStyle(Color.foregroundSecondary)
}
.padding()
.background(Color.backgroundPrimary) // white in light mode, black in darkA theme's page color keeps a little of its hue: Color.proOrange.backgroundPrimary is _50, a faint orange, in light mode, where the app token Color.backgroundPrimary is white.
Sources#
- The library's ColorTokens.swift, which defines the 20 tokens and the stops each one uses.
- The library's UniformRamp.swift, which sets the lightness of each stop.
- W3C, WCAG 2.2: contrast minimum.
See also
- Why use ColorTokensKit over native colors? What you gain over SwiftUI's system colors, asset catalogs and hex values, and what it costs.
- 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.
- Using your first tokens Color text, buttons, cards and lists with tokens, then theme a movie card in one line.
- Color theory Pair stops that always read, and pick secondary hues that match your brand, in both modes.
- How accessible is it? What the palette guarantees for contrast in every hue and mode, and what you still need to check.