Managing dark mode mostly means letting the tokens do it: you get dark mode without writing any dark-mode code. Each semantic token holds two stops, one for light mode and one for dark, and SwiftUI draws the one for the current mode:
let theme = Color.proOrange
VStack(alignment: .leading, spacing: 4) {
Text("Lisbon")
.font(.headline)
.foregroundStyle(theme.foregroundPrimary) // _1000 in light mode, _50 in dark
Text("3 nights from October 12")
.font(.subheadline)
.foregroundStyle(theme.foregroundSecondary) // _800 and _200
}
.padding()
.background(theme.backgroundSecondary) // _100 and _800
.padding()
.background(theme.backgroundPrimary) // _50 and _1000Every foreground token passes WCAG AA on the page in both modes.
Tokens switch, stops stay fixed#
A stop is one color in both modes, so where a token belongs, it fails quietly in dark mode. The first line uses a stop, the second a token:
let theme = Color.proOrange
VStack(alignment: .leading) {
Text("3 nights from October 12")
.foregroundStyle(theme._800.toColor()) // 8.72:1 in light mode, 1.79:1 in dark
Text("3 nights from October 12")
.foregroundStyle(theme.foregroundSecondary) // 8.72:1 and 11.48:1
}
.padding()
.background(theme.backgroundPrimary)Use a stop only when a color should look the same in both modes, such as a logo. A contrast check that reads a token sees only one of its stops; contrastRatio(to:method:) shows how to check both.
Dark backgrounds keep a tint of their hue#
let theme = Color.proSky
VStack(alignment: .leading, spacing: 4) {
Text("Reykjavík")
.font(.headline)
.foregroundStyle(theme.foregroundPrimary)
Text("3 nights from October 12")
.font(.subheadline)
.foregroundStyle(theme.foregroundSecondary)
}
.padding()
.background(theme.backgroundPrimary) // _1000 in dark mode, a very dark blue
.border(theme.outlineSecondary)
.padding()
.background(Color.backgroundPrimary) // black in dark modeA family's backgrounds come from its own ramp, so the Reykjavík card stays blue in the dark. For a neutral page, use Color.backgroundPrimary, which is black. Every family's foreground tokens pass AA on the gray background tokens too. What does equal lightness cost? explains the tint.
Your own light and dark pairs#
Color(light:dark:) turns any two colors into one that switches with the mode, the way every token does:
// Color(light:dark:) takes two Colors, so each stop needs toColor().
let highlight = Color(
light: Color.proYellow._200.toColor(),
dark: Color.proYellow._800.toColor()
)
let ink = Color(light: Color(hex: "#1A1A2E"), dark: .white)
Text("Your flight to Kyoto boards at 14:30.")
.foregroundStyle(ink)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(highlight)The text measures 11.47:1 in light mode and 9.52:1 in dark. The result resolves when SwiftUI draws it, so it follows .preferredColorScheme(.dark) without your code reading colorScheme. UIKit and AppKit get the same initializer, as UIColor(light:dark:) and NSColor(light:dark:).
Color functions follow the mode#
Color functions such as soften() work out their result each time a color is drawn, so a function applied to a token keeps adapting:
let subtitle = Color.proOrange.foregroundSecondary // _800 in light mode, _200 in dark
VStack(alignment: .leading, spacing: 4) {
Text("3 nights from October 12")
.font(.subheadline)
.foregroundStyle(subtitle)
Text("Flights included")
.font(.footnote)
.foregroundStyle(subtitle.soften()) // _750 and _250: quieter in both modes
}
.padding()
.background(Color.proOrange.backgroundPrimary)soften() moves toward the background, so it's quieter in both modes. lighten() would give _750 and _150 here: quieter on a light page, louder on a dark one. Harmonies and gradients of tokens adapt too.
Increase Contrast and watchOS#
Tokens don't change with Increase Contrast; high contrast modes shows how to strengthen them when it's on. Apple Watch always draws in dark mode, so tokens use their dark stops there, as watchOS explains.
Sources#
- Apple, UIColor init(dynamicProvider:) and NSColor init(name:dynamicProvider:), the dynamic colors tokens are built on.
- Apple, preferredColorScheme(_:).
- Apple, Human Interface Guidelines: Dark Mode.
- W3C, WCAG 2.2: contrast minimum.
- The library's Color+Dynamic.swift and Color+Adaptive.swift.
Next steps
- Building for interaction states Give buttons and rows hover, pressed, selected and disabled colors that stay readable in both modes.
- Setting up themes Recolor a view, a screen or your whole app from one value, with the same contrast in every hue.
- Color(light:dark:) Make any color switch between light and dark mode, the way every token does.
- How accessible is it? What the palette guarantees for contrast in every hue and mode, and what you still need to check.