Skip to content
ColorTokensKitby Penguin Design Ventures
Contents
Tutorial

Using your first tokens

Color text, buttons, cards and lists with tokens, then theme a movie card in one line.

Updated View as Markdown

Color text, buttons, cards and lists by the job each color does, and each one follows light and dark mode on its own.

Color text by importance#

swift
VStack(alignment: .leading, spacing: 4) {
    Text("Lisbon")
        .font(.headline)
        .foregroundStyle(Color.foregroundPrimary)     // _1000 in light mode, _50 in dark
    Text("3 nights from October 12")
        .font(.subheadline)
        .foregroundStyle(Color.foregroundSecondary)   // _800 in light mode, _200 in dark
    Text("Updated 2 min ago")
        .font(.footnote)
        .foregroundStyle(Color.foregroundTertiary)    // _700 in light mode, _300 in dark
}
light mode
Lisbon3 nights from October 12Updated 2 min ago

This assumes you've added the package and copied ColorTokens.swift. If you haven't, set it up first.

These are the gray tokens on Color. Even tertiary, the faintest, measures 9.55:1 or more against the page in both modes.

Fill or outline a button#

swift
let brand = ProTheme(hex: "#00B386")

HStack {
    Button { } label: {
        Text("Book now")
            .padding(.vertical, 10)
            .padding(.horizontal, 16)
            .foregroundStyle(brand.invertedForegroundPrimary)   // _50 in light mode, _1000 in dark
            .background(brand.invertedBackgroundPrimary)        // _900 in light mode, _50 in dark
    }
    Button { } label: {
        Text("Details")
            .padding(.vertical, 10)
            .padding(.horizontal, 16)
            .foregroundStyle(brand.foregroundPrimary)
            .border(brand.outlinePrimary)                       // _600 in light mode, _350 in dark
    }
}
.buttonStyle(.plain)
light mode
Book nowDetails

For color, take the tokens from a family, here one made from the brand's hex. A filled button takes the inverted tokens. An outlined one takes outlinePrimary, which clears 3:1 against the page, so people can find the control's edge.

Lay out a card#

swift
let theme = Color.proOrange   // Lisbon's color

VStack(alignment: .leading, spacing: 12) {
    VStack(alignment: .leading, spacing: 4) {
        Text("Lisbon")
            .font(.headline)
            .foregroundStyle(theme.foregroundPrimary)
        Text("3 nights from October 12")
            .font(.subheadline)
            .foregroundStyle(theme.foregroundSecondary)
    }
    HStack(spacing: 6) {
        ForEach(["Beach", "City"], id: \.self) { tag in
            Text(tag)
                .font(.caption)
                .padding(.vertical, 4)
                .padding(.horizontal, 8)
                .foregroundStyle(theme.foregroundPrimary)
                .background(theme.backgroundTertiary)   // _200 in light mode, _700 in dark
        }
    }
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(theme.backgroundPrimary)                    // _50 in light mode, _1000 in dark
light mode
Lisbon3 nights from October 12
BeachCity

The card takes the page color, backgroundPrimary, and its tags sit a layer up on backgroundTertiary. Text on the tags takes foregroundPrimary, because foregroundSecondary drops to 4.48:1 there in dark mode.

Build a list row#

swift
struct SettingRow: View {
    let title: String
    let value: String

    var body: some View {
        VStack(spacing: 0) {
            HStack {
                Text(title)
                    .foregroundStyle(Color.foregroundPrimary)
                Spacer()
                Text(value)
                    .foregroundStyle(Color.foregroundSecondary)
                Image(systemName: "chevron.right")
                    .foregroundStyle(Color.foregroundTertiary)
            }
            .padding()
            Rectangle()
                .fill(Color.outlineSecondary)   // _200 in light mode, _800 in dark
                .frame(height: 1)
        }
    }
}

VStack(spacing: 0) {
    SettingRow(title: "Price alerts", value: "On")
    SettingRow(title: "Home airport", value: "LIS")
}
light mode
Price alertsOn›
Home airportLIS›

The label is primary, the value secondary and the chevron tertiary. The divider takes an outline token, which is faint on purpose.

Build a movie card#

swift
struct MovieCard: View {
    let title: String
    let details: String
    let match: String
    let poster: String
    let theme: ProTheme

    var body: some View {
        HStack(alignment: .top, spacing: 14) {
            AsyncImage(url: URL(string: "https://picsum.photos/seed/\(poster)/300/450")) { image in
                image.resizable().scaledToFill()
            } placeholder: {
                theme.backgroundTertiary
            }
            .frame(width: 96, height: 144)
            .clipShape(.rect(cornerRadius: 16))

            VStack(alignment: .leading, spacing: 10) {
                Text(match)
                    .font(.caption.bold())
                    .padding(.vertical, 4)
                    .padding(.horizontal, 10)
                    .foregroundStyle(theme.foregroundPrimary)
                    .background(theme.backgroundTertiary, in: .capsule)
                VStack(alignment: .leading, spacing: 2) {
                    Text(title)
                        .font(.title3.bold())
                        .foregroundStyle(theme.foregroundPrimary)
                    Text(details)
                        .font(.subheadline)
                        .foregroundStyle(theme.foregroundSecondary)
                }
                Label("Watch trailer", systemImage: "play.fill")
                    .font(.subheadline.bold())
                    .padding(.vertical, 8)
                    .padding(.horizontal, 14)
                    .foregroundStyle(theme.invertedForegroundPrimary)
                    .background(theme.invertedBackgroundTertiary, in: .capsule)
            }
            Spacer(minLength: 0)
        }
        .padding(12)
        .background(
            [theme.surfaceTertiary, theme.surfacePrimary].proGradient(),
            in: .rect(cornerRadius: 28)
        )
        .overlay(RoundedRectangle(cornerRadius: 28).strokeBorder(theme.outlineTertiary))
    }
}

MovieCard(
    title: "Sintra Summer", details: "2024 · 1h 48m · Romance",
    match: "🍿 92% match", poster: "sintra", theme: Color.proRose
)
light mode
🍿 92% match
Sintra Summer2024 · 1h 48m · Romance
Watch trailer

A movie app could show a card like this for each recommendation, and every color in it comes from one theme. The match pill sits on backgroundTertiary, the trailer button uses the inverted tokens, and the wash is a gradient between two translucent surface tokens. The details keep 7.4:1 or more even where the wash is strongest, in every hue. The poster's corners are the card's radius minus its padding, so the two curves stay parallel.

Theme it in one line#

Pass each movie a theme that suits its mood, and nothing else changes:

swift
VStack(spacing: 12) {
    MovieCard(
        title: "Night Train to Porto", details: "2023 · 2h 4m · Thriller",
        match: "🍿 88% match", poster: "porto", theme: Color.proIndigo
    )
    MovieCard(
        title: "The Lisbon Bakery", details: "2025 · 1h 31m · Comedy",
        match: "🍿 95% match", poster: "bakery", theme: Color.proAmber
    )
}
light mode
🍿 88% match
Night Train to Porto2023 · 2h 4m · Thriller
Watch trailer
🍿 95% match
The Lisbon Bakery2025 · 1h 31m · Comedy
Watch trailer

Every family has the same lightness at each stop, so the text keeps its contrast in every theme. Setting up themes takes this to a whole screen.

Next steps