Recolor a view, a screen or your whole app by changing one value. Draw with a family's semantic tokens instead of fixed colors, and the contrast stays the same in every hue, in light and dark mode.
Theme one view#
Give the view a theme, draw it with the theme's tokens, and pass any family:
struct TripCard: View {
let destination: String
let theme: ProTheme
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(destination)
.font(.headline)
.foregroundStyle(theme.foregroundPrimary)
Text("3 nights from October 12")
.font(.subheadline)
.foregroundStyle(theme.foregroundSecondary)
}
.padding()
.background(theme.backgroundPrimary)
.border(theme.outlineSecondary)
}
}
VStack {
TripCard(destination: "Lisbon", theme: Color.proOrange)
TripCard(destination: "Kyoto", theme: Color.proPink)
TripCard(destination: "Reykjavík", theme: Color.proSky)
}The subtitle is 8.72:1 on the card in light mode and 11.48:1 in dark, whichever hue you pass. Pick another family, or type your brand's hex:
Lisbon
3 nights from October 12
Lisbon
3 nights from October 12
A theme is one hue, so you can store it with @AppStorage and let people pick their own.
Theme a screen#
When a whole screen shares a theme, put it in the SwiftUI environment, and one modifier sets it for every view inside:
extension EnvironmentValues {
@Entry var theme: ProTheme = Color.proBlue
}
struct Tag: View {
@Environment(\.theme) private var theme
let title: String
var body: some View {
Text(title)
.font(.subheadline)
.padding(.vertical, 4)
.padding(.horizontal, 10)
.foregroundStyle(theme.foregroundPrimary)
.background(theme.backgroundTertiary)
}
}
struct TripScreen: View {
@Environment(\.theme) private var theme
var body: some View {
VStack(alignment: .leading) {
Text("Lisbon")
.font(.headline)
.foregroundStyle(theme.foregroundPrimary)
Text("3 nights from October 12")
.font(.subheadline)
.foregroundStyle(theme.foregroundSecondary)
HStack {
Tag(title: "Beach")
Tag(title: "City")
Tag(title: "Food")
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(theme.backgroundPrimary)
}
}
TripScreen()
.environment(\.theme, Color.proOrange)Tag reads the nearest theme from the environment. @Entry needs Xcode 16; with an older Xcode, write an EnvironmentKey, and because ProTheme is Sendable, it can be the key's default value.
Theme the whole app from your brand color#
Make a family from your brand's hex value:
extension Color {
static let brand = ProTheme(hex: "#00B386")
}Then set it once, where your app creates its window:
@main
struct TripsApp: App {
var body: some Scene {
WindowGroup {
TripScreen()
.environment(\.theme, Color.brand)
}
}
}Your brand family has all 20 stops and every token, with the same contrast as the built-in families. Color.brand.toColor() is still exactly #00B386, for your logo, and a gray hex gives you a gray family. Replacing your app colors moves the rest of your colors over.
Add a matching second theme#
A harmony of your brand family is a whole family too. complement is the one across the hue wheel; give it to the part of your app that should stand apart:
extension Color {
static let brandSecondary = brand.complement // hue 347°, across the wheel from 167°
}
VStack(spacing: 0) {
TripScreen()
TripScreen()
.environment(\.theme, Color.brandSecondary)
}
.environment(\.theme, Color.brand)The nearest .environment wins, so the second screen takes the secondary theme. Color theory has more hues that go with yours.
See also
- ProTheme Make a theme from a hex, a hue or OKLCH numbers, and every stop and token keeps its contrast.
- Ready-made themes 37 themes to use as they are, from proPink to proRose, each with 20 stops and every token.
- Managing dark mode Get dark mode without extra code: tokens switch stops for you, and your own colors can too.
- What does the library offer? 36 color themes, ready-to-use tokens with dark mode, a theme for your brand, and state functions.