Skip to content
ColorTokensKitby Penguin Design Ventures
Contents
How-to

Setting up themes

Recolor a view, a screen or your whole app from one value, with the same contrast in every hue.

Updated View as Markdown

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:

swift
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)
}
light mode
Lisbon3 nights from October 12
Kyoto3 nights from October 12
Reykjavík3 nights from October 12

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:

Try it / Theme a view
Theme
light mode

Lisbon

3 nights from October 12

dark mode

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:

swift
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)
light mode
Lisbon3 nights from October 12
BeachCityFood

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:

#00B386
swift
extension Color {
    static let brand = ProTheme(hex: "#00B386")
}

Then set it once, where your app creates its window:

swift
@main
struct TripsApp: App {
    var body: some Scene {
        WindowGroup {
            TripScreen()
                .environment(\.theme, Color.brand)
        }
    }
}
light mode
Lisbon3 nights from October 12
BeachCityFood

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:

swift
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)
#00B386 · light mode
Lisbon3 nights from October 12
BeachCityFood
ProTheme.primary(forHue: 346.99) · light mode
Lisbon3 nights from October 12
BeachCityFood

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.