# Components and examples

Copy the common mobile components, from buttons to paywalls, each built from tokens for both modes.

Every component below is built from one theme's tokens, so it follows dark mode and keeps its contrast in any hue. They start small and get more involved.

## Small pieces

### Primary button

Use it for the one main action on a screen: `invertedBackgroundPrimary` behind `invertedForegroundPrimary` is the strongest pair a theme has, and `soften(by: 2)` shows the press. CrosswordChef's primary button works this way.

```swift
struct PrimaryButtonStyle: ButtonStyle {
    var theme: ProTheme = Color.proGray

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .font(.headline)
            .foregroundStyle(theme.invertedForegroundPrimary)
            .frame(maxWidth: .infinity)
            .padding(.vertical, 14)
            .background(
                configuration.isPressed
                    ? theme.invertedBackgroundPrimary.soften(by: 2)
                    : theme.invertedBackgroundPrimary,
                in: .rect(cornerRadius: 14)
            )
    }
}

Button("Book flight") {}
    .buttonStyle(PrimaryButtonStyle(theme: brand))
```

What this draws, with `#00B386`:

| Color                       | light mode                             | dark mode                             |
| --------------------------- | -------------------------------------- | ------------------------------------- |
| `invertedForegroundPrimary` | #eaf8f2                                | color(display-p3 0.0176 0.1298 0.086) |
| `invertedBackgroundPrimary` | color(display-p3 0.0474 0.2188 0.1544) | #eaf8f2                               |

### Secondary button

Use it beside a primary button for the other choice. It stays gray, with the app tokens on `Color`, so the brand is left for the primary action: an `outlinePrimary` border and a `foregroundPrimary` label, and `backgroundSecondary` while pressed.

```swift
struct SecondaryButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .font(.headline)
            .foregroundStyle(Color.foregroundPrimary)
            .frame(maxWidth: .infinity)
            .padding(.vertical, 14)
            .background(
                configuration.isPressed ? Color.backgroundSecondary : .clear,
                in: .rect(cornerRadius: 14)
            )
            .overlay {
                RoundedRectangle(cornerRadius: 14)
                    .strokeBorder(Color.outlinePrimary, lineWidth: 1.5)
            }
    }
}

Button("Compare fares") {}
    .buttonStyle(SecondaryButtonStyle())
```

What this draws, with `Color`:

| Color               | light mode | dark mode |
| ------------------- | ---------- | --------- |
| `foregroundPrimary` | #000001    | #ffffff   |
| `outlinePrimary`    | #626262    | #b1b1b1   |

### Theme buttons

Use a theme's own buttons on a screen that belongs to that theme, like a game mode: bold is `invertedBackgroundTertiary` with `invertedForegroundPrimary`, and pastel is `surfacePrimary` with `foregroundSecondary`. CrosswordChef's bold and pastel buttons are these two, here in the Mini's cobalt.

```swift
let mini = Color.proCobalt

HStack(spacing: 12) {
    Button("Play today") {}
        .foregroundStyle(mini.invertedForegroundPrimary)
        .frame(maxWidth: .infinity)
        .padding(.vertical, 12)
        .background(mini.invertedBackgroundTertiary, in: .capsule)
    Button("Archive") {}
        .foregroundStyle(mini.foregroundSecondary)
        .frame(maxWidth: .infinity)
        .padding(.vertical, 12)
        .background(mini.surfacePrimary, in: .capsule)
}
.font(.headline)
.buttonStyle(.plain)
```

What this draws, with `Color.proCobalt`:

| Color                        | light mode                                   | dark mode                              |
| ---------------------------- | -------------------------------------------- | -------------------------------------- |
| `invertedForegroundPrimary`  | color(display-p3 0.942 0.9618 0.9992)        | #0d1c35                                |
| `invertedBackgroundTertiary` | #3d67ae                                      | color(display-p3 0.6807 0.787 0.9957)  |
| `foregroundSecondary`        | #274577                                      | color(display-p3 0.7493 0.8335 0.9966) |
| `surfacePrimary`             | color(display-p3 0.7493 0.8335 0.9966 / 0.5) | rgb(54 92 155 / 0.5)                   |

### Filter chips

Use chips to narrow a list by more than one thing: each filter has its own theme, and a selected chip fills with that theme's `backgroundTertiary`, with a check and an X to remove it. The rest stay gray, on `Color.backgroundSecondary`. CrosswordChef's pill selector works the same way.

```swift
struct FilterChips: View {
    let filters = [("Flights", Color.proBlue), ("Hotels", Color.proOrange),
                   ("Trains", Color.proGreen), ("Food", Color.proPink)]
    @State private var selected: Set<String> = ["Flights", "Hotels"]

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 8) {
                ForEach(filters.indices, id: \.self) { chip(filters[$0].0, theme: filters[$0].1) }
            }
        }
        .buttonStyle(.plain)
    }

    private func chip(_ name: String, theme: ProTheme) -> some View {
        let isSelected = selected.contains(name)
        return Button {
            if isSelected { selected.remove(name) } else { selected.insert(name) }
        } label: {
            HStack(spacing: 6) {
                if isSelected { Image(systemName: "checkmark").fontWeight(.bold) }
                Text(name)
                if isSelected {
                    Image(systemName: "xmark")
                        .font(.caption.bold())
                        .foregroundStyle(theme.foregroundSecondary)
                }
            }
            .font(.subheadline.weight(.medium))
            .padding(.horizontal, 14)
            .padding(.vertical, 8)
            .foregroundStyle(isSelected ? theme.foregroundPrimary : Color.foregroundSecondary)
            .background(isSelected ? theme.backgroundTertiary : Color.backgroundSecondary,
                        in: .capsule)
        }
    }
}
```

What this draws, with `Color`:

| Color                        | light mode                             | dark mode                              |
| ---------------------------- | -------------------------------------- | -------------------------------------- |
| `blue.backgroundTertiary`    | color(display-p3 0.7202 0.8416 0.9967) | #215f98                                |
| `blue.foregroundPrimary`     | #051d33                                | color(display-p3 0.9359 0.9638 0.9992) |
| `blue.foregroundSecondary`   | #174874                                | color(display-p3 0.7202 0.8416 0.9967) |
| `orange.backgroundTertiary`  | color(display-p3 0.978 0.79 0.6716)    | #90491a                                |
| `orange.foregroundPrimary`   | #301404                                | color(display-p3 0.9936 0.9528 0.9271) |
| `orange.foregroundSecondary` | #6e3612                                | color(display-p3 0.978 0.79 0.6716)    |
| `foregroundSecondary`        | #2c2c2c                                | #dddddd                                |
| `backgroundSecondary`        | #f4f5f5                                | #2c2c2c                                |

### Count badge

Use a badge to count what's new on an icon: a capsule from a second theme, here `Color.proRed`'s `invertedBackgroundTertiary` with its `invertedForegroundPrimary`, stays readable in both modes.

```swift
struct BadgedIcon: View {
    let symbol: String
    let count: Int

    var body: some View {
        Image(systemName: symbol)
            .font(.title2)
            .foregroundStyle(brand.foregroundPrimary)
            .padding(8)
            .overlay(alignment: .topTrailing) {
                Text("\(count)")
                    .font(.caption2.bold())
                    .foregroundStyle(Color.proRed.invertedForegroundPrimary)
                    .padding(.horizontal, 5)
                    .frame(minWidth: 17, minHeight: 17)
                    .background(Color.proRed.invertedBackgroundTertiary, in: .capsule)
            }
    }
}

HStack(spacing: 24) {
    BadgedIcon(symbol: "bell", count: 3)
    BadgedIcon(symbol: "envelope", count: 12)
}
```

What this draws, with `#00B386`:

| Color                            | light mode                            | dark mode                             |
| -------------------------------- | ------------------------------------- | ------------------------------------- |
| `foregroundPrimary`              | color(display-p3 0.0176 0.1298 0.086) | #eaf8f2                               |
| `red.invertedForegroundPrimary`  | color(display-p3 0.998 0.9491 0.9461) | #321213                               |
| `red.invertedBackgroundTertiary` | #a74b4e                               | color(display-p3 0.9956 0.7059 0.696) |

### Avatar

Use avatars for people, and overlap them for a group: a photo where there is one, and otherwise the initials in `foregroundSecondary` on `backgroundTertiary`, in a hue picked from the name, so each person keeps one color. A ring in `Color.backgroundPrimary` keeps overlapping faces apart. CrosswordChef's avatars fall back to colored initials too.

```swift
struct Avatar: View {
    let name: String
    var photoURL: URL? = nil

    var body: some View {
        // The same name lands on the same one of the 36 hues every time.
        let seed = name.unicodeScalars.reduce(0) { $0 + Int($1.value) }
        let theme = ProTheme.primary(forHue: Double(seed % 36 * 10))

        AsyncImage(url: photoURL) { image in
            image.resizable().scaledToFill()
        } placeholder: {
            Text(name.split(separator: " ").compactMap(\.first).prefix(2).map(String.init).joined())
                .font(.subheadline.weight(.semibold))
                .foregroundStyle(theme.foregroundSecondary)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(theme.backgroundTertiary)
        }
        .frame(width: 40, height: 40)
        .clipShape(.circle)
        .padding(2)
        .background(Color.backgroundPrimary, in: .circle)
    }
}

HStack(spacing: -10) {
    Avatar(name: "Ana Souza", photoURL: URL(string: "https://picsum.photos/seed/ana/80"))
    Avatar(name: "Lea Park")      // hue 200
    Avatar(name: "Sam Okafor", photoURL: URL(string: "https://picsum.photos/seed/sam/80"))
    Avatar(name: "Noor Haddad")   // hue 40
    Avatar(name: "Mia Chen", photoURL: URL(string: "https://picsum.photos/seed/mia/80"))
    Text("+3")
        .font(.subheadline.weight(.semibold))
        .foregroundStyle(Color.foregroundSecondary)
        .frame(width: 40, height: 40)
        .background(Color.backgroundTertiary, in: .circle)
        .padding(2)
        .background(Color.backgroundPrimary, in: .circle)
}
```

What this draws, with `Color`:

| Color                         | light mode                             | dark mode                              |
| ----------------------------- | -------------------------------------- | -------------------------------------- |
| `backgroundPrimary`           | #ffffff                                | #000000                                |
| `hue:200.backgroundTertiary`  | #8ee1e6                                | color(display-p3 0.0241 0.406 0.4333)  |
| `hue:200.foregroundSecondary` | color(display-p3 0.0135 0.3071 0.3285) | #8ee1e6                                |
| `hue:40.backgroundTertiary`   | color(display-p3 0.9883 0.7829 0.6987) | #93462a                                |
| `hue:40.foregroundSecondary`  | #70341e                                | color(display-p3 0.9883 0.7829 0.6987) |
| `backgroundTertiary`          | #dddddd                                | #454545                                |
| `foregroundSecondary`         | #2c2c2c                                | #dddddd                                |

### Checkbox

Use checkboxes for options that are each on or off: only a checked box takes the theme, filling with `invertedBackgroundTertiary` under an `invertedForegroundPrimary` check. An empty box is a gray `Color.outlinePrimary` square, and the labels stay in `Color.foregroundPrimary`.

```swift
struct Checkbox: View {
    let label: String
    @Binding var isOn: Bool
    var theme: ProTheme = Color.proIndigo

    var body: some View {
        Button { isOn.toggle() } label: {
            HStack(spacing: 12) {
                RoundedRectangle(cornerRadius: 6)
                    .fill(isOn ? theme.invertedBackgroundTertiary : .clear)
                    .strokeBorder(isOn ? .clear : Color.outlinePrimary, lineWidth: 1.5)
                    .frame(width: 22, height: 22)
                    .overlay {
                        Image(systemName: "checkmark")
                            .font(.caption.weight(.heavy))
                            .foregroundStyle(theme.invertedForegroundPrimary)
                            .opacity(isOn ? 1 : 0)
                    }
                Text(label).foregroundStyle(Color.foregroundPrimary)
            }
        }
        .buttonStyle(.plain)
    }
}

VStack(alignment: .leading, spacing: 12) {
    Checkbox(label: "Window seat", isOn: .constant(true))
    Checkbox(label: "Extra legroom", isOn: .constant(false))
}
```

What this draws, with `Color`:

| Color                               | light mode                             | dark mode                              |
| ----------------------------------- | -------------------------------------- | -------------------------------------- |
| `indigo.invertedBackgroundTertiary` | #4f64af                                | color(display-p3 0.7152 0.7771 0.9957) |
| `indigo.invertedForegroundPrimary`  | color(display-p3 0.9477 0.9599 0.9992) | #131a35                                |
| `foregroundPrimary`                 | #000001                                | #ffffff                                |
| `outlinePrimary`                    | #626262                                | #b1b1b1                                |

### Toggle row

Use a toggle row for a setting that takes effect right away: the row sits on `backgroundSecondary`, and the switch takes the theme's `_500` stop, which keeps the white knob visible in both modes.

```swift
Toggle(isOn: .constant(true)) {
    VStack(alignment: .leading, spacing: 2) {
        Text("Price alerts")
            .foregroundStyle(brand.foregroundPrimary)
        Text("When fares to Lisbon drop")
            .font(.subheadline)
            .foregroundStyle(brand.foregroundSecondary)
    }
}
.tint(brand._500.toColor())
.padding()
.background(brand.backgroundSecondary, in: .rect(cornerRadius: 16))
```

What this draws, with `#00B386`:

| Color                 | light mode                             | dark mode                              |
| --------------------- | -------------------------------------- | -------------------------------------- |
| `backgroundSecondary` | #d2f1e3                                | color(display-p3 0.0797 0.3134 0.2262) |
| `foregroundPrimary`   | color(display-p3 0.0176 0.1298 0.086)  | #eaf8f2                                |
| `foregroundSecondary` | color(display-p3 0.0797 0.3134 0.2262) | #9ce2c5                                |
| `_500`                | color(display-p3 0.1935 0.624 0.4631)  | color(display-p3 0.1935 0.624 0.4631)  |

### Text field

Use a labeled field for typed input. At rest it's all gray, from the app tokens on `Color`: a `foregroundSecondary` label, a `foregroundTertiary` placeholder and an `outlinePrimary` border, which reaches the 3:1 a field's edge needs. Only the field with focus takes the brand, as a thicker `invertedBackgroundTertiary` outline.

```swift
struct LabeledField: View {
    let label: String
    let placeholder: String
    @Binding var text: String
    @FocusState private var isFocused: Bool

    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            Text(label)
                .font(.subheadline.weight(.medium))
                .foregroundStyle(Color.foregroundSecondary)
            TextField(label, text: $text,
                      prompt: Text(placeholder).foregroundStyle(Color.foregroundTertiary))
                .focused($isFocused)
                .foregroundStyle(Color.foregroundPrimary)
                .padding(12)
                .overlay {
                    RoundedRectangle(cornerRadius: 12).strokeBorder(
                        isFocused ? brand.invertedBackgroundTertiary : Color.outlinePrimary,
                        lineWidth: isFocused ? 2 : 1
                    )
                }
        }
    }
}

LabeledField(label: "From", placeholder: "City or airport", text: .constant("Lisbon"))
LabeledField(label: "To", placeholder: "City or airport", text: .constant(""))
```

What this draws, with `Color`:

| Color                                | light mode                             | dark mode |
| ------------------------------------ | -------------------------------------- | --------- |
| `foregroundSecondary`                | #2c2c2c                                | #dddddd   |
| `foregroundPrimary`                  | #000001                                | #ffffff   |
| `#00B386.invertedBackgroundTertiary` | color(display-p3 0.1368 0.4655 0.3425) | #79dab5   |
| `foregroundTertiary`                 | #454545                                | #c0c0c0   |
| `outlinePrimary`                     | #626262                                | #b1b1b1   |

### Progress bar

Use a progress bar for steps toward a goal: the track is `backgroundTertiary` and the fill `invertedBackgroundTertiary`. CrosswordChef's progress bar uses the same track.

```swift
struct ProgressBar: View {
    let value: Double   // 0 to 1

    var body: some View {
        Capsule()
            .fill(brand.backgroundTertiary)
            .overlay(alignment: .leading) {
                GeometryReader { proxy in
                    Capsule()
                        .fill(brand.invertedBackgroundTertiary)
                        .frame(width: proxy.size.width * value)
                }
            }
            .frame(height: 8)
    }
}

VStack(alignment: .leading, spacing: 8) {
    HStack {
        Text("Packing list")
        Spacer()
        Text("6 of 10").foregroundStyle(brand.foregroundSecondary)
    }
    .font(.subheadline)
    .foregroundStyle(brand.foregroundPrimary)
    ProgressBar(value: 0.6)
}
```

What this draws, with `#00B386`:

| Color                        | light mode                             | dark mode                              |
| ---------------------------- | -------------------------------------- | -------------------------------------- |
| `foregroundPrimary`          | color(display-p3 0.0176 0.1298 0.086)  | #eaf8f2                                |
| `foregroundSecondary`        | color(display-p3 0.0797 0.3134 0.2262) | #9ce2c5                                |
| `backgroundTertiary`         | #9ce2c5                                | color(display-p3 0.1149 0.4143 0.3028) |
| `invertedBackgroundTertiary` | color(display-p3 0.1368 0.4655 0.3425) | #79dab5                                |

### Progress ring

Use a ring for one number out of a whole, like today's goal. The track is a gray `Color.backgroundTertiary`, and the arc is an angular gradient whose hue turns once around the circle: [`rotateHue(by:)`](https://colortokenskit.com/api/rotate-hue/index.md) turns `proPink._450`, the palette's most vivid middle, in 60° steps at the same lightness, and [`proAngularGradient()`](https://colortokenskit.com/api/pro-angular-gradient/index.md) blends them. CrosswordChef's progress circle has the same shape.

```swift
struct ProgressRing: View {
    let value: Double   // 0 to 1

    var body: some View {
        // One lightness, with the hue turning once around the ring.
        let start = Color.proPink._450.toColor()
        let hues = stride(from: 0.0, to: 360, by: 60).map { start.rotateHue(by: .degrees($0)) }

        ZStack {
            Circle()
                .stroke(Color.backgroundTertiary, lineWidth: 8)
            Circle()
                .trim(from: 0, to: value)
                .stroke(hues.proAngularGradient(),
                        style: StrokeStyle(lineWidth: 8, lineCap: .round))
                .rotationEffect(.degrees(-90))
            Text(value, format: .percent.precision(.fractionLength(0)))
                .font(.headline.monospacedDigit())
                .foregroundStyle(Color.foregroundPrimary)
        }
        .frame(width: 72, height: 72)
    }
}

ProgressRing(value: 0.72)
```

What this draws, with `Color`:

| Color                | light mode                             | dark mode                              |
| -------------------- | -------------------------------------- | -------------------------------------- |
| `backgroundTertiary` | #dddddd                                | #454545                                |
| `pink._450`          | #eb6f9a                                | #eb6f9a                                |
| `hue:60._450`        | #e17f09                                | #e17f09                                |
| `hue:120._450`       | color(display-p3 0.5582 0.6341 0.1883) | color(display-p3 0.5582 0.6341 0.1883) |
| `hue:180._450`       | color(display-p3 0.0652 0.6772 0.5895) | color(display-p3 0.0652 0.6772 0.5895) |
| `hue:240._450`       | color(display-p3 0.261 0.6202 0.9084)  | color(display-p3 0.261 0.6202 0.9084)  |
| `hue:300._450`       | #ad83f0                                | #ad83f0                                |
| `foregroundPrimary`  | #000001                                | #ffffff                                |

It draws a angular gradient from `pink._450` to `hue:60._450` to `hue:120._450` to `hue:180._450` to `hue:240._450` to `hue:300._450`, with `.vivid` and `.smooth`.

## Rows, cards and bars

### Settings list

Group settings on a `Color.backgroundPrimary` card over `Color.backgroundSecondary`, with `outlineTertiary` hairlines between rows. Each icon tile takes its row's own theme: a solid `backgroundTertiary` tile under a `foregroundSecondary` symbol.

```swift
func row(_ icon: String, _ tint: ProTheme, _ label: String, _ value: String) -> some View {
    HStack(spacing: 12) {
        Image(systemName: icon)
            .foregroundStyle(tint.foregroundSecondary)
            .frame(width: 30, height: 30)
            .background(tint.backgroundTertiary, in: .rect(cornerRadius: 7))
        Text(label).foregroundStyle(Color.foregroundPrimary)
        Spacer()
        Text(value).foregroundStyle(Color.foregroundSecondary)
        Image(systemName: "chevron.right").foregroundStyle(Color.foregroundTertiary)
    }
    .padding(.horizontal, 16)
    .frame(height: 52)
}

VStack(spacing: 0) {
    row("bell.fill", Color.proRed, "Notifications", "On")
    Divider().overlay(Color.outlineTertiary).padding(.leading, 58)
    row("airplane", Color.proBlue, "Home airport", "LIS")
    Divider().overlay(Color.outlineTertiary).padding(.leading, 58)
    row("moon.fill", Color.proIndigo, "Appearance", "Auto")
}
.background(Color.backgroundPrimary, in: .rect(cornerRadius: 12))
.padding()
.background(Color.backgroundSecondary)
```

What this draws, with `Color`:

| Color                        | light mode                             | dark mode                              |
| ---------------------------- | -------------------------------------- | -------------------------------------- |
| `backgroundSecondary`        | #f4f5f5                                | #2c2c2c                                |
| `backgroundPrimary`          | #ffffff                                | #000000                                |
| `red.backgroundTertiary`     | color(display-p3 0.9952 0.7729 0.763)  | #954245                                |
| `red.foregroundSecondary`    | #723133                                | color(display-p3 0.9952 0.7729 0.763)  |
| `foregroundPrimary`          | #000001                                | #ffffff                                |
| `foregroundSecondary`        | #2c2c2c                                | #dddddd                                |
| `foregroundTertiary`         | #454545                                | #c0c0c0                                |
| `blue.backgroundTertiary`    | color(display-p3 0.7202 0.8416 0.9967) | #215f98                                |
| `blue.foregroundSecondary`   | #174874                                | color(display-p3 0.7202 0.8416 0.9967) |
| `indigo.backgroundTertiary`  | color(display-p3 0.7757 0.8257 0.9966) | #45599d                                |
| `indigo.foregroundSecondary` | #344378                                | color(display-p3 0.7757 0.8257 0.9966) |

### Segmented control

Switch between a few views of one list. It's navigation, so it stays gray, with the app tokens on `Color`: a `backgroundSecondary` track, and the selected segment on `backgroundPrimary` in light mode and `backgroundTertiary` in dark, so it sits above the track in both.

```swift
struct TripFilter: View {
    @State private var selection = "Upcoming"
    let selectedFill = Color(light: Color.backgroundPrimary, dark: Color.backgroundTertiary)

    var body: some View {
        HStack(spacing: 2) {
            ForEach(["Upcoming", "Past", "Saved"], id: \.self) { option in
                let selected = option == selection
                Button { selection = option } label: {
                    Text(option)
                        .font(.subheadline.weight(.semibold))
                        .foregroundStyle(Color.foregroundPrimary)
                        .frame(maxWidth: .infinity, minHeight: 32)
                        .background(selected ? selectedFill : .clear, in: .rect(cornerRadius: 7))
                        .contentShape(.rect)
                }
            }
        }
        .buttonStyle(.plain)
        .padding(2)
        .background(Color.backgroundSecondary, in: .rect(cornerRadius: 9))
    }
}
```

What this draws, with `Color`:

| Color                                  | light mode | dark mode |
| -------------------------------------- | ---------- | --------- |
| `backgroundSecondary`                  | #f4f5f5    | #2c2c2c   |
| `foregroundPrimary`                    | #000001    | #ffffff   |
| `backgroundPrimary/backgroundTertiary` | #ffffff    | #454545   |

### Search bar

Put search at the top of a list, on `Color.backgroundSecondary` with a `foregroundTertiary` prompt and a clear button once there's a query.

```swift
struct SearchBar: View {
    @State private var query = ""

    var body: some View {
        HStack(spacing: 8) {
            Image(systemName: "magnifyingglass")
                .foregroundStyle(Color.foregroundTertiary)
            TextField("Search", text: $query, prompt: Text("Cities or airports")
                .foregroundStyle(Color.foregroundTertiary))
                .foregroundStyle(Color.foregroundPrimary)
            if !query.isEmpty {
                Button("Clear", systemImage: "xmark.circle.fill") { query = "" }
                    .labelStyle(.iconOnly)
                    .foregroundStyle(Color.foregroundTertiary)
                    .buttonStyle(.plain)
            }
        }
        .padding(.horizontal, 10)
        .frame(height: 40)
        .background(Color.backgroundSecondary, in: .rect(cornerRadius: 10))
    }
}
```

What this draws, with `Color`:

| Color                 | light mode | dark mode |
| --------------------- | ---------- | --------- |
| `backgroundSecondary` | #f4f5f5    | #2c2c2c   |
| `foregroundTertiary`  | #454545    | #c0c0c0   |
| `foregroundPrimary`   | #000001    | #ffffff   |

### Tinted banner

Point to something new or time-sensitive with a card in one theme's `surfacePrimary` and text in its foreground tokens. CrosswordChef's `BannerComponent` works this way.

```swift
struct Banner: View {
    let theme: ProTheme, icon: String, title: String, subtitle: String

    var body: some View {
        HStack(spacing: 12) {
            Image(systemName: icon)
                .font(.title3)
                .foregroundStyle(theme.foregroundSecondary)
            VStack(alignment: .leading, spacing: 2) {
                Text(title).font(.headline).foregroundStyle(theme.foregroundPrimary)
                Text(subtitle).font(.subheadline).foregroundStyle(theme.foregroundSecondary)
            }
            Spacer()
            Image(systemName: "chevron.right").foregroundStyle(theme.foregroundTertiary)
        }
        .padding(14)
        .background(theme.surfacePrimary, in: .rect(cornerRadius: 14))
    }
}

Banner(theme: Color.proGreen, icon: "airplane",
       title: "Check-in is open", subtitle: "Flight TP 1331 leaves at 9:40")
Banner(theme: Color.proGold, icon: "star.fill",
       title: "You earned a free night", subtitle: "Use it before March")
```

What this draws:

| Color                       | light mode                             | dark mode                                    |
| --------------------------- | -------------------------------------- | -------------------------------------------- |
| `green.surfacePrimary`      | rgb(171 224 181 / 0.5)                 | rgb(28 106 53 / 0.5)                         |
| `green.foregroundSecondary` | #135027                                | #abe0b5                                      |
| `green.foregroundPrimary`   | #04210c                                | #edf8ee                                      |
| `green.foregroundTertiary`  | #1c6a35                                | #73d089                                      |
| `gold.surfacePrimary`       | rgb(241 207 151 / 0.5)                 | color(display-p3 0.4701 0.3381 0.0194 / 0.5) |
| `gold.foregroundSecondary`  | color(display-p3 0.3572 0.2539 0.0127) | #f1cf97                                      |
| `gold.foregroundPrimary`    | color(display-p3 0.1518 0.0999 0.0024) | #fbf4e9                                      |
| `gold.foregroundTertiary`   | color(display-p3 0.4701 0.3381 0.0194) | #eab34d                                      |

### Toast

Confirm an action with a pill in `invertedBackgroundPrimary` that floats over any screen. The check sits in a circle of `proGreen`'s `invertedBackgroundTertiary`, drawn in its `invertedForegroundPrimary`, so the success reads at a glance in both modes. CrosswordChef's toast uses the same inverted pair.

```swift
struct Toast: View {
    let message: String

    var body: some View {
        HStack(spacing: 10) {
            Image(systemName: "checkmark")
                .font(.caption.weight(.heavy))
                .foregroundStyle(Color.proGreen.invertedForegroundPrimary)
                .frame(width: 24, height: 24)
                .background(Color.proGreen.invertedBackgroundTertiary, in: .circle)
            Text(message)
                .font(.subheadline.weight(.semibold))
                .foregroundStyle(Color.invertedForegroundPrimary)
        }
        .padding(.vertical, 8)
        .padding(.leading, 8)
        .padding(.trailing, 18)
        .background(Color.invertedBackgroundPrimary, in: .capsule)
        .shadow(color: .black.opacity(0.15), radius: 12, y: 6)
    }
}

Toast(message: "Saved to your trips")
```

What this draws, with `Color`:

| Color                              | light mode | dark mode |
| ---------------------------------- | ---------- | --------- |
| `invertedBackgroundPrimary`        | #171718    | #ffffff   |
| `green.invertedBackgroundTertiary` | #21773d    | #91d8a0   |
| `green.invertedForegroundPrimary`  | #edf8ee    | #04210c   |
| `invertedForegroundPrimary`        | #ffffff    | #000001   |

### Stat tiles

Show a few numbers at a glance, each tile a two-color gradient from its theme's `surfacePrimary` to the `surfacePrimary` of the hue 40° along, with an icon in the theme's `invertedBackgroundTertiary`. Every hue has the same lightness at each stop, so the text keeps its contrast across the whole gradient. CrosswordChef's post-game stat cards give each card its own theme too.

```swift
struct StatTile: View {
    let value: String, label: String, icon: String, theme: ProTheme

    var body: some View {
        let neighbor = theme.rotateHue(by: .degrees(40))
        VStack(alignment: .leading, spacing: 4) {
            HStack(alignment: .firstTextBaseline) {
                Text(value)
                    .font(.title2.bold().monospacedDigit())
                    .foregroundStyle(theme.foregroundPrimary)
                Spacer()
                Image(systemName: icon).foregroundStyle(theme.invertedBackgroundTertiary)
            }
            Text(label)
                .font(.footnote)
                .foregroundStyle(theme.foregroundSecondary)
        }
        .padding(14)
        .background(
            [theme.surfacePrimary, neighbor.surfacePrimary]
                .proGradient(from: .topLeading, to: .bottomTrailing),
            in: .rect(cornerRadius: 14)
        )
    }
}

Grid(horizontalSpacing: 10, verticalSpacing: 10) {
    GridRow {
        StatTile(value: "12", label: "Countries", icon: "globe", theme: Color.proOrange)
        StatTile(value: "48", label: "Nights away", icon: "moon.fill", theme: Color.proIndigo)
    }
    GridRow {
        StatTile(value: "31k", label: "Kilometers flown", icon: "airplane", theme: Color.proSky)
        StatTile(value: "7", label: "Trips this year", icon: "suitcase.fill", theme: Color.proPink)
    }
}
```

What this draws:

| Color                               | light mode                                   | dark mode                                    |
| ----------------------------------- | -------------------------------------------- | -------------------------------------------- |
| `orange.surfacePrimary`             | color(display-p3 0.978 0.79 0.6716 / 0.5)    | rgb(144 73 26 / 0.5)                         |
| `mustard.surfacePrimary`            | rgb(232 210 150 / 0.5)                       | color(display-p3 0.4412 0.3517 0.0185 / 0.5) |
| `orange.foregroundPrimary`          | #301404                                      | color(display-p3 0.9936 0.9528 0.9271)       |
| `orange.invertedBackgroundTertiary` | #a15320                                      | color(display-p3 0.9744 0.7286 0.573)        |
| `orange.foregroundSecondary`        | #6e3612                                      | color(display-p3 0.978 0.79 0.6716)          |
| `indigo.surfacePrimary`             | color(display-p3 0.7757 0.8257 0.9966 / 0.5) | rgb(69 89 157 / 0.5)                         |
| `purple.surfacePrimary`             | rgb(231 200 255 / 0.5)                       | rgb(115 76 143 / 0.5)                        |
| `indigo.foregroundPrimary`          | #131a35                                      | color(display-p3 0.9477 0.9599 0.9992)       |
| `indigo.invertedBackgroundTertiary` | #4f64af                                      | color(display-p3 0.7152 0.7771 0.9957)       |
| `indigo.foregroundSecondary`        | #344378                                      | color(display-p3 0.7757 0.8257 0.9966)       |
| `sky.surfacePrimary`                | rgb(154 221 253 / 0.5)                       | color(display-p3 0.0889 0.3874 0.5384 / 0.5) |
| `sky.foregroundPrimary`             | color(display-p3 0.0119 0.1193 0.1786)       | #eaf7fe                                      |
| `sky.invertedBackgroundTertiary`    | color(display-p3 0.1087 0.4359 0.6024)       | #77d3fe                                      |
| `sky.foregroundSecondary`           | color(display-p3 0.0596 0.2924 0.4106)       | #9addfd                                      |
| `pink.surfacePrimary`               | color(display-p3 0.9839 0.7697 0.8336 / 0.5) | rgb(146 66 94 / 0.5)                         |
| `coral.surfacePrimary`              | color(display-p3 0.9883 0.7829 0.6987 / 0.5) | rgb(147 70 42 / 0.5)                         |
| `pink.foregroundPrimary`            | #31111c                                      | color(display-p3 0.9953 0.9483 0.9611)       |
| `pink.invertedBackgroundTertiary`   | #a34b6a                                      | color(display-p3 0.9812 0.702 0.7888)        |
| `pink.foregroundSecondary`          | #6f3147                                      | color(display-p3 0.9839 0.7697 0.8336)       |

It draws a linear gradient from `orange.surfacePrimary` to `mustard.surfacePrimary`, with `.vivid` and `.smooth`; a linear gradient from `indigo.surfacePrimary` to `purple.surfacePrimary`, with `.vivid` and `.smooth`; a linear gradient from `sky.surfacePrimary` to `indigo.surfacePrimary`, with `.vivid` and `.smooth`; a linear gradient from `pink.surfacePrimary` to `coral.surfacePrimary`, with `.vivid` and `.smooth`.

### Content card

List a trip, a product or an article on a `backgroundSecondary` card. A photo looks the same in both modes, so its stand-in is a soft `.analogous` gradient from the fixed stop `_300`, set 2 points in from the card's edge with a corner radius 2 points smaller, so the corners stay concentric. CrosswordChef's archive cards put a small crossword grid where the photo is.

```swift
struct TripCard: View {
    let theme = Color.proCoral

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            Rectangle()
                .fill(theme._300.proGradient(.analogous))
                .frame(height: 120)
                .overlay {
                    Image(systemName: "photo").font(.largeTitle).foregroundStyle(.white)
                }
                .clipShape(.rect(cornerRadius: 14))
                .padding(2)
            HStack(alignment: .top) {
                VStack(alignment: .leading, spacing: 4) {
                    Text("Lisbon, Portugal").font(.headline)
                    Text("3 nights from October 12").font(.subheadline)
                        .foregroundStyle(theme.foregroundSecondary)
                }
                Spacer()
                VStack(alignment: .trailing, spacing: 4) {
                    Text("€420").font(.headline)
                    Text("total").font(.subheadline).foregroundStyle(theme.foregroundSecondary)
                }
            }
            .foregroundStyle(theme.foregroundPrimary)
            .padding(14)
        }
        .background(theme.backgroundSecondary, in: .rect(cornerRadius: 16))
    }
}
```

What this draws, with `Color.proCoral`:

| Color                 | light mode                             | dark mode                              |
| --------------------- | -------------------------------------- | -------------------------------------- |
| `backgroundSecondary` | color(display-p3 0.9938 0.8974 0.8576) | #70341e                                |
| `_300`                | color(display-p3 0.9845 0.6518 0.5175) | color(display-p3 0.9845 0.6518 0.5175) |
| `white`               | #ffffff                                | #ffffff                                |
| `foregroundPrimary`   | #311308                                | color(display-p3 0.9962 0.9513 0.9327) |
| `foregroundSecondary` | #70341e                                | color(display-p3 0.9883 0.7829 0.6987) |

It draws a linear gradient from the `.analogous` recipe from `_300`, with `.vivid` and `.smooth`.

### Chat bubbles

Tell your messages from theirs: yours in the theme's `invertedBackgroundTertiary`, theirs in `Color.backgroundSecondary`. CrosswordChef's support chat uses bubbles like these.

```swift
struct Bubble: View {
    let text: String
    let isMine: Bool
    let theme = Color.proIris

    var body: some View {
        Text(text)
            .foregroundStyle(isMine ? theme.invertedForegroundPrimary : Color.foregroundPrimary)
            .padding(.vertical, 10)
            .padding(.horizontal, 14)
            .background(isMine ? theme.invertedBackgroundTertiary : Color.backgroundSecondary,
                        in: .rect(cornerRadius: 18))
            .padding(isMine ? .leading : .trailing, 48)
            .frame(maxWidth: .infinity, alignment: isMine ? .trailing : .leading)
    }
}

VStack(spacing: 14) {
    Bubble(text: "Hi! Can I move my hotel by one night?", isMine: true)
    Bubble(text: "Of course. Which nights would you like?", isMine: false)
    Bubble(text: "October 13 to 16, please.", isMine: true)
}
.padding()
```

What this draws, with `Color.proIris`:

| Color                        | light mode                             | dark mode                             |
| ---------------------------- | -------------------------------------- | ------------------------------------- |
| `invertedForegroundPrimary`  | color(display-p3 0.9534 0.9581 0.9992) | #181935                               |
| `invertedBackgroundTertiary` | #5e60ae                                | color(display-p3 0.7481 0.767 0.9957) |
| `gray.foregroundPrimary`     | #000001                                | #ffffff                               |
| `gray.backgroundSecondary`   | #f4f5f5                                | #2c2c2c                               |

### Tab bar

Float a tab bar over your content with the look of iOS 26's Liquid Glass, made only from gradients: a `Color.backgroundPrimary` fill that thins to 75% in the middle, a 1-point white sheen that fades to 25% at both ends, and a capsule inset from the screen's sides. The other tabs stay outlined in `Color.foregroundSecondary`; the selected tab fills its icon and label in `Color.foregroundPrimary`, on a white pill that turns `Color.backgroundTertiary` in dark mode.

```swift
struct GlassTabBar: View {
    let tabs = [("house", "Home"), ("magnifyingglass", "Search"),
                ("suitcase", "Trips"), ("person", "Profile")]
    var selected = "Trips"

    let glass = [
        Color.backgroundPrimary, Color.backgroundPrimary.opacity(0.75), Color.backgroundPrimary,
    ].proGradient(from: .leading, to: .trailing)
    let sheen = [Color.white.opacity(0.25), Color.white, Color.white.opacity(0.25)]
        .proGradient(from: .leading, to: .trailing)
    let pill = Color(light: Color.backgroundPrimary, dark: Color.backgroundTertiary)

    var body: some View {
        HStack(spacing: 0) {
            ForEach(tabs, id: \.1) { tab in
                let (icon, title) = tab
                let isSelected = title == selected
                VStack(spacing: 2) {
                    Image(systemName: icon).symbolVariant(isSelected ? .fill : .none)
                    Text(title).font(.caption2.weight(.medium))
                }
                .foregroundStyle(isSelected ? Color.foregroundPrimary : Color.foregroundSecondary)
                .frame(maxWidth: .infinity, minHeight: 48)
                .background(isSelected ? pill : .clear, in: .capsule)
            }
        }
        .padding(6)
        .background(glass, in: .capsule)
        .overlay { Capsule().strokeBorder(sheen, lineWidth: 1) }
        .padding(.horizontal, 16)
    }
}

// Float it over your content: ScrollView { … }.safeAreaInset(edge: .bottom) { GlassTabBar() }
```

What this draws, with `Color.proPink`:

| Color                                          | light mode              | dark mode               |
| ---------------------------------------------- | ----------------------- | ----------------------- |
| `_400`                                         | #fa7ba7                 | #fa7ba7                 |
| `app.backgroundPrimary`                        | #ffffff                 | #000000                 |
| `app.backgroundPrimary@75`                     | rgb(255 255 255 / 0.75) | rgb(0 0 0 / 0.75)       |
| `white@25`                                     | rgb(255 255 255 / 0.25) | rgb(255 255 255 / 0.25) |
| `white`                                        | #ffffff                 | #ffffff                 |
| `app.foregroundSecondary`                      | #2c2c2c                 | #dddddd                 |
| `app.backgroundPrimary/app.backgroundTertiary` | #ffffff                 | #454545                 |
| `app.foregroundPrimary`                        | #000001                 | #ffffff                 |

It draws a linear gradient from the `.analogous` recipe from `_400`, with `.vivid` and `.smooth`; a linear gradient from `app.backgroundPrimary` to `app.backgroundPrimary@75` to `app.backgroundPrimary`, with `.vivid` and `.smooth`; a linear gradient from `white@25` to `white` to `white@25`, with `.vivid` and `.smooth`.

## Whole features

### Leaderboard

Use a leaderboard to rank players: the top three wear medal themes, `proGold`, `proGray` and `proBrown`, as soft `surfacePrimary` circles with the medal's `foregroundPrimary` number, each player shows their photo, and your own row sits on `proYellow.surfacePrimary`. CrosswordChef's board rows work this way.

```swift
struct LeaderboardRow: View {
    let rank: Int
    let name: String
    let time: String
    var isYou = false

    var medal: ProTheme? { [1: Color.proGold, 2: Color.proGray, 3: Color.proBrown][rank] }

    var body: some View {
        HStack(spacing: 12) {
            Text("\(rank)")
                .font(.footnote.bold().monospacedDigit())
                .foregroundStyle(medal?.foregroundPrimary ?? Color.foregroundSecondary)
                .frame(width: 28, height: 28)
                .background(medal?.surfacePrimary ?? .clear, in: .circle)
            AsyncImage(url: URL(string: "https://picsum.photos/seed/\(name)/80")) { image in
                image.resizable().scaledToFill()
            } placeholder: {
                Color.backgroundTertiary
            }
            .frame(width: 36, height: 36)
            .clipShape(.circle)
            Text(name).fontWeight(isYou ? .semibold : .regular)
            Spacer()
            Text(time).monospacedDigit()
        }
        .padding(.horizontal, 12)
        .frame(height: 56)
        .foregroundStyle(Color.foregroundPrimary)
        .background(isYou ? Color.proYellow.surfacePrimary : .clear, in: .rect(cornerRadius: 12))
    }
}

VStack(spacing: 2) {
    LeaderboardRow(rank: 1, name: "Maya", time: "0:48")
    LeaderboardRow(rank: 2, name: "Kenji", time: "0:55")
    LeaderboardRow(rank: 3, name: "Ana", time: "1:02")
    LeaderboardRow(rank: 14, name: "You", time: "1:31", isYou: true)
}
```

What this draws, with `Color`:

| Color                     | light mode                             | dark mode                                    |
| ------------------------- | -------------------------------------- | -------------------------------------------- |
| `gold.surfacePrimary`     | rgb(241 207 151 / 0.5)                 | color(display-p3 0.4701 0.3381 0.0194 / 0.5) |
| `gold.foregroundPrimary`  | color(display-p3 0.1518 0.0999 0.0024) | #fbf4e9                                      |
| `foregroundPrimary`       | #000001                                | #ffffff                                      |
| `gray.surfacePrimary`     | rgb(221 221 221 / 0.5)                 | rgb(69 69 69 / 0.5)                          |
| `gray.foregroundPrimary`  | #000001                                | #ffffff                                      |
| `brown.surfacePrimary`    | rgb(254 202 160 / 0.5)                 | rgb(139 77 3 / 0.5)                          |
| `brown.foregroundPrimary` | #2e1600                                | #fef3ea                                      |
| `yellow.surfacePrimary`   | rgb(223 213 151 / 0.5)                 | color(display-p3 0.4109 0.3641 0.0185 / 0.5) |
| `foregroundSecondary`     | #2c2c2c                                | #dddddd                                      |

### Streak calendar

Use a streak calendar to show a habit day by day, one tall capsule per day. Finished days fill with a soft `proGreen` gradient from `backgroundSecondary` to `backgroundTertiary` and show a check. A day you can still play sits on gray `backgroundSecondary` with a play icon and a sheen border that runs top to bottom, `outlineTertiary` into a `proGreen.invertedBackgroundTertiary` highlight and back. Today gets a 2-point outline in the same green, and days ahead sit on gray `surfacePrimary` behind a lock. CrosswordChef's archive calendar works this way.

```swift
enum DayState { case solved, open, today, upcoming }

struct StreakDay: View {
    let date: Int
    let state: DayState
    private let green = Color.proGreen

    var body: some View {
        VStack(spacing: 10) {
            Text("\(date)").font(.caption.bold().monospacedDigit())
            Image(systemName: icon).font(.caption.bold())
        }
        .foregroundStyle(ink)
        .frame(width: 40, height: 76)
        .background(fill, in: .capsule)
        .overlay(Capsule().strokeBorder(border, lineWidth: state == .today ? 2 : 1.5))
    }

    var icon: String {
        switch state {
        case .solved: "checkmark"
        case .open, .today: "play.fill"
        case .upcoming: "lock.fill"
        }
    }

    var ink: Color {
        switch state {
        case .solved: green.foregroundPrimary
        case .open, .today: Color.foregroundPrimary
        case .upcoming: Color.foregroundTertiary
        }
    }

    var fill: AnyShapeStyle {
        switch state {
        case .solved:
            AnyShapeStyle([green.backgroundSecondary, green.backgroundTertiary].proGradient())
        case .open, .today: AnyShapeStyle(Color.backgroundSecondary)
        case .upcoming: AnyShapeStyle(Color.surfacePrimary)
        }
    }

    var border: AnyShapeStyle {
        switch state {
        case .open:
            let sheen = [
                green.outlineTertiary, green.invertedBackgroundTertiary, green.outlineTertiary,
            ]
            return AnyShapeStyle(sheen.proGradient())
        case .today: return AnyShapeStyle(green.invertedBackgroundTertiary)
        case .solved, .upcoming: return AnyShapeStyle(Color.clear)
        }
    }
}

HStack(spacing: 8) {
    StreakDay(date: 8, state: .solved)
    StreakDay(date: 9, state: .solved)
    StreakDay(date: 10, state: .open)
    StreakDay(date: 11, state: .solved)
    StreakDay(date: 12, state: .solved)
    StreakDay(date: 13, state: .today)
    StreakDay(date: 14, state: .upcoming)
}
```

What this draws, with `Color`:

| Color                              | light mode             | dark mode           |
| ---------------------------------- | ---------------------- | ------------------- |
| `green.backgroundSecondary`        | #d8f0dc                | #135027             |
| `green.backgroundTertiary`         | #abe0b5                | #1c6a35             |
| `green.foregroundPrimary`          | #04210c                | #edf8ee             |
| `green.outlineTertiary`            | #d8f0dc                | #0b381a             |
| `green.invertedBackgroundTertiary` | #21773d                | #91d8a0             |
| `backgroundSecondary`              | #f4f5f5                | #2c2c2c             |
| `foregroundPrimary`                | #000001                | #ffffff             |
| `surfacePrimary`                   | rgb(221 221 221 / 0.5) | rgb(69 69 69 / 0.5) |
| `foregroundTertiary`               | #454545                | #c0c0c0             |

It draws a linear gradient from `green.backgroundSecondary` to `green.backgroundTertiary`, with `.vivid` and `.smooth`; a linear gradient from `green.backgroundSecondary` to `green.backgroundTertiary`, with `.vivid` and `.smooth`; a linear gradient from `green.outlineTertiary` to `green.invertedBackgroundTertiary` to `green.outlineTertiary`, with `.vivid` and `.smooth`; a linear gradient from `green.backgroundSecondary` to `green.backgroundTertiary`, with `.vivid` and `.smooth`; a linear gradient from `green.backgroundSecondary` to `green.backgroundTertiary`, with `.vivid` and `.smooth`.

### Plan picker

Use a plan picker on an upgrade sheet: only the chosen plan wears the theme, `proIndigo` here, with a `surfacePrimary` fill and an `invertedBackgroundTertiary` outline and check, while the other plan and the title stay gray, drawn from `Color.proGray`, the family the app tokens use. The button repeats the outline's color. CrosswordChef's upgrade sheet is built this way.

```swift
struct PlanTile: View {
    let name: String
    let price: String
    let isSelected: Bool

    var theme: ProTheme { isSelected ? Color.proIndigo : Color.proGray }

    var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 2) {
                Text(name).font(.headline).foregroundStyle(theme.foregroundPrimary)
                Text(price).font(.subheadline).foregroundStyle(theme.foregroundSecondary)
            }
            Spacer()
            Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
                .font(.title2)
                .foregroundStyle(
                    isSelected ? theme.invertedBackgroundTertiary : theme.outlinePrimary
                )
        }
        .padding(16)
        .background(isSelected ? theme.surfacePrimary : .clear, in: .rect(cornerRadius: 16))
        .overlay {
            RoundedRectangle(cornerRadius: 16).strokeBorder(
                isSelected ? theme.invertedBackgroundTertiary : theme.outlineSecondary,
                lineWidth: isSelected ? 2 : 1
            )
        }
    }
}

VStack(spacing: 12) {
    Text("Travel with Pro").font(.title.bold()).foregroundStyle(Color.foregroundPrimary)
    PlanTile(name: "Yearly", price: "$19.99 a year", isSelected: true)
    PlanTile(name: "Monthly", price: "$3.99 a month", isSelected: false)
    Button("Continue") {}
        .font(.headline)
        .frame(maxWidth: .infinity, minHeight: 50)
        .foregroundStyle(Color.proIndigo.invertedForegroundPrimary)
        .background(Color.proIndigo.invertedBackgroundTertiary, in: .capsule)
}
.padding(20)
```

What this draws, with `Color.proIndigo`:

| Color                        | light mode                                   | dark mode                              |
| ---------------------------- | -------------------------------------------- | -------------------------------------- |
| `gray.foregroundPrimary`     | #000001                                      | #ffffff                                |
| `surfacePrimary`             | color(display-p3 0.7757 0.8257 0.9966 / 0.5) | rgb(69 89 157 / 0.5)                   |
| `invertedBackgroundTertiary` | #4f64af                                      | color(display-p3 0.7152 0.7771 0.9957) |
| `foregroundPrimary`          | #131a35                                      | color(display-p3 0.9477 0.9599 0.9992) |
| `foregroundSecondary`        | #344378                                      | color(display-p3 0.7757 0.8257 0.9966) |
| `invertedForegroundPrimary`  | color(display-p3 0.9477 0.9599 0.9992)       | #131a35                                |
| `gray.outlineSecondary`      | #dddddd                                      | #2c2c2c                                |
| `gray.foregroundSecondary`   | #2c2c2c                                      | #dddddd                                |
| `gray.outlinePrimary`        | #626262                                      | #b1b1b1                                |

### Onboarding step

Use one step per screen when you onboard people, here in `proPink`: capsules in `invertedBackgroundTertiary` over `backgroundTertiary` show how far along they are, the body text is `foregroundSecondary` and sits close under the title, and one button moves on. CrosswordChef's onboarding steps share this layout.

```swift
struct OnboardingStep: View {
    let step: Int
    let count: Int
    private let theme = Color.proPink
    private var done: Color { theme.invertedBackgroundTertiary }
    private var todo: Color { theme.backgroundTertiary }

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            HStack(spacing: 6) {
                ForEach(0..<count, id: \.self) { index in
                    Capsule().fill(index <= step ? done : todo).frame(height: 4)
                }
            }
            VStack(alignment: .leading, spacing: 6) {
                Text("Where to next?").font(.largeTitle.bold())
                Text("Pick three places you'd love to see, and we'll watch prices for you.")
                    .foregroundStyle(theme.foregroundSecondary)
            }
            Button("Continue") {}
                .font(.headline)
                .frame(maxWidth: .infinity, minHeight: 50)
                .foregroundStyle(theme.invertedForegroundPrimary)
                .background(theme.invertedBackgroundTertiary, in: .capsule)
        }
        .padding(24)
        .foregroundStyle(theme.foregroundPrimary)
    }
}

OnboardingStep(step: 1, count: 4)
```

What this draws, with `Color.proPink`:

| Color                        | light mode                             | dark mode                              |
| ---------------------------- | -------------------------------------- | -------------------------------------- |
| `invertedBackgroundTertiary` | #a34b6a                                | color(display-p3 0.9812 0.702 0.7888)  |
| `backgroundTertiary`         | color(display-p3 0.9839 0.7697 0.8336) | #92425e                                |
| `foregroundPrimary`          | #31111c                                | color(display-p3 0.9953 0.9483 0.9611) |
| `foregroundSecondary`        | #6f3147                                | color(display-p3 0.9839 0.7697 0.8336) |
| `invertedForegroundPrimary`  | color(display-p3 0.9953 0.9483 0.9611) | #31111c                                |

### Crossword grid

Keep a game grid gray and let color mean something: squares use the app's gray tokens, the current word is `backgroundTertiary`, the focused square gets a `foregroundPrimary` outline, blocks are a fixed `proGray._950`, and a solved square turns `proGreen`'s `invertedBackgroundTertiary` with `invertedForegroundPrimary` letters. CrosswordChef's grid squares and clue bar have the same states.

```swift
enum Square { case empty, block, word, focused, solved }

struct GridSquare: View {
    let letter: String
    let square: Square
    private let solved = Color.proGreen

    var body: some View {
        Text(letter)
            .font(.title2.weight(.semibold))
            .frame(width: 48, height: 48)
            .foregroundStyle(colors.text)
            .background(colors.fill)
            .border(Color.outlinePrimary, width: 0.5)
            .overlay {
                if square == .focused {
                    Rectangle().strokeBorder(Color.foregroundPrimary, lineWidth: 2)
                }
            }
    }

    var colors: (fill: Color, text: Color) {
        switch square {
        case .empty: (Color.backgroundSecondary, Color.foregroundPrimary)
        case .block: (Color.proGray._950.toColor(), .clear)
        case .word, .focused: (Color.backgroundTertiary, Color.foregroundPrimary)
        case .solved: (solved.invertedBackgroundTertiary, solved.invertedForegroundPrimary)
        }
    }
}

let clueBar = HStack {
    Image(systemName: "chevron.left")
    Text("3A French capital").font(.subheadline.weight(.semibold))
    Spacer()
    Image(systemName: "chevron.right")
}
.padding(14)
.foregroundStyle(Color.foregroundPrimary)
.background(Color.backgroundTertiary)
```

What this draws, with `Color`:

| Color                              | light mode | dark mode |
| ---------------------------------- | ---------- | --------- |
| `gray._950`                        | #0d0d0d    | #0d0d0d   |
| `outlinePrimary`                   | #626262    | #b1b1b1   |
| `green.invertedBackgroundTertiary` | #21773d    | #91d8a0   |
| `green.invertedForegroundPrimary`  | #edf8ee    | #04210c   |
| `backgroundSecondary`              | #f4f5f5    | #2c2c2c   |
| `backgroundTertiary`               | #dddddd    | #454545   |
| `foregroundPrimary`                | #000001    | #ffffff   |

### Game keyboard

Use your own keyboard when a game takes letters: the keys are white on a `backgroundSecondary` tray in light mode and lift to `backgroundTertiary` in dark mode, one color made with `Color(light:dark:)`. CrosswordChef's keyboard does the same.

```swift
struct GameKeyboard: View {
    let rows = ["QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"]
    let key = Color(light: Color.backgroundPrimary, dark: Color.backgroundTertiary)

    var body: some View {
        VStack(spacing: 8) {
            ForEach(rows, id: \.self) { row in
                HStack(spacing: 4) {
                    ForEach(Array(row), id: \.self) { letter in
                        Text(String(letter))
                            .font(.title3)
                            .frame(width: 26, height: 40)
                            .background(key, in: .rect(cornerRadius: 6))
                    }
                }
            }
        }
        .padding(6)
        .foregroundStyle(Color.foregroundPrimary)
        .background(Color.backgroundSecondary)
    }
}
```

What this draws, with `Color`:

| Color                                  | light mode | dark mode |
| -------------------------------------- | ---------- | --------- |
| `backgroundSecondary`                  | #f4f5f5    | #2c2c2c   |
| `backgroundPrimary/backgroundTertiary` | #ffffff    | #454545   |
| `foregroundPrimary`                    | #000001    | #ffffff   |

### Game card

Use a card per course, game or feature on a home feed, here in `proOrange`: a gentle `proGradient` from the theme's `surfaceTertiary` to `surfacePrimary` keeps it light, the time pill sits on `backgroundPrimary`, the stat pills on `surfacePrimary`, and the button is `invertedBackgroundTertiary`. CrosswordChef's home feed cards follow this recipe, one theme per game.

```swift
struct LessonCard: View {
    let theme: ProTheme

    var body: some View {
        VStack(alignment: .leading, spacing: 14) {
            HStack(alignment: .top) {
                VStack(alignment: .leading, spacing: 2) {
                    Text("Portuguese").font(.title2.bold())
                    Text("Lesson 4: Ordering at a café")
                        .font(.subheadline)
                        .foregroundStyle(theme.foregroundSecondary)
                }
                Spacer()
                pill("5 min", "clock", theme.backgroundPrimary)
            }
            HStack {
                pill("12 days", "flame.fill", theme.surfacePrimary)
                pill("320 XP", "bolt.fill", theme.surfacePrimary)
            }
            Button("Continue lesson", systemImage: "play.fill") {}
                .font(.headline)
                .frame(maxWidth: .infinity, minHeight: 48)
                .foregroundStyle(theme.invertedForegroundPrimary)
                .background(theme.invertedBackgroundTertiary, in: .capsule)
        }
        .padding(20)
        .foregroundStyle(theme.foregroundPrimary)
        .background([theme.surfaceTertiary, theme.surfacePrimary].proGradient(),
                    in: .rect(cornerRadius: 24))
    }

    func pill(_ text: String, _ icon: String, _ fill: Color) -> some View {
        Label(text, systemImage: icon)
            .font(.subheadline.weight(.semibold))
            .lineLimit(1)
            .padding(.horizontal, 10)
            .padding(.vertical, 6)
            .background(fill, in: .capsule)
    }
}

LessonCard(theme: Color.proOrange)
```

What this draws, with `Color.proOrange`:

| Color                        | light mode                                | dark mode                              |
| ---------------------------- | ----------------------------------------- | -------------------------------------- |
| `surfaceTertiary`            | color(display-p3 0.978 0.79 0.6716 / 0.1) | rgb(144 73 26 / 0.1)                   |
| `surfacePrimary`             | color(display-p3 0.978 0.79 0.6716 / 0.5) | rgb(144 73 26 / 0.5)                   |
| `foregroundPrimary`          | #301404                                   | color(display-p3 0.9936 0.9528 0.9271) |
| `foregroundSecondary`        | #6e3612                                   | color(display-p3 0.978 0.79 0.6716)    |
| `backgroundPrimary`          | color(display-p3 0.9936 0.9528 0.9271)    | #301404                                |
| `invertedBackgroundTertiary` | #a15320                                   | color(display-p3 0.9744 0.7286 0.573)  |
| `invertedForegroundPrimary`  | color(display-p3 0.9936 0.9528 0.9271)    | #301404                                |

It draws a linear gradient from `surfaceTertiary` to `surfacePrimary`, with `.vivid` and `.smooth`.

### Share card

Use a share card for an image people post outside your app: it's built from fixed stops, not tokens, a `proGradient` from `_700` to `_950` with `_50` text, so it looks the same whatever mode the sender is in. CrosswordChef's share cards are made this way.

```swift
struct StreakCard: View {
    let theme: ProTheme
    let days: Int

    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text("MY STREAK")
                .font(.caption.weight(.bold))
                .tracking(1.5)
                .foregroundStyle(theme._200.toColor())
            Spacer()
            VStack(alignment: .leading, spacing: -10) {
                Text("\(days)")
                    .font(.system(size: 88, weight: .heavy, design: .rounded))
                Text("days in a row").font(.title3.weight(.semibold))
            }
            Spacer()
            Label("Daily Mini", systemImage: "square.grid.2x2.fill")
                .font(.footnote.weight(.semibold))
                .foregroundStyle(theme._200.toColor())
        }
        .foregroundStyle(theme._50.toColor())
        .padding(24)
        .frame(width: 280, height: 280, alignment: .leading)
        .background([theme._700, theme._950].proGradient(), in: .rect(cornerRadius: 28))
    }
}

StreakCard(theme: Color.proPurple, days: 42)
```

What this draws, with `Color.proPurple`:

| Color  | light mode | dark mode |
| ------ | ---------- | --------- |
| `_700` | #734c8f    | #734c8f   |
| `_950` | #311e3e    | #311e3e   |
| `_200` | #e7c8ff    | #e7c8ff   |
| `_50`  | #f9f3fe    | #f9f3fe   |

It draws a linear gradient from `_700` to `_950`, with `.vivid` and `.smooth`.

### Charts

Use Swift Charts for activity, trends and breakdowns, and draw every series at its theme's `_500`, the vivid middle of the ramp: on the gray `backgroundSecondary` card it clears the 3:1 that WCAG asks of chart marks in both modes. The bars take their colors from `theme.triad`, each fare line gets its own theme with the cheapest route's area fading to clear, and every chart keeps a legend as a second cue, as [using tokens in charts](https://colortokenskit.com/advanced/charts/index.md) explains.

```swift
struct TripCharts: View {
    let theme: ProTheme
    let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
    let modes = ["Walk", "Bike", "Train"]
    let kilometers = [[5.2, 7.1, 4.8, 9.4, 6.9, 11.0, 8.3],
                      [3.0, 6.5, 8.2, 4.1, 5.5, 12.4, 6.0],
                      [12.0, 4.5, 9.8, 14.2, 6.3, 18.5, 7.2]]
    let cities = ["Paris", "London", "Berlin"]
    let fares = [[168.0, 175, 160, 164, 150, 158, 141],
                 [212.0, 198, 205, 186, 171, 179, 152],
                 [245.0, 238, 229, 236, 219, 204, 198]]
    let names = ["Stays", "Flights", "Food"]
    let amounts = [540.0, 420, 260]

    var modeColors: [Color] { theme.triad.map { $0._500.toColor() } }
    var cityColors: [Color] {
        [Color.proPink, Color.proIndigo, Color.proGold].map { $0._500.toColor() }
    }
    var budgetColors: [Color] {
        [Color.proBlue, Color.proCoral, Color.proGrass].map { $0._500.toColor() }
    }

    var body: some View {
        VStack(spacing: 12) {
            card("Kilometers this week", "171 km") {
                Chart {
                    ForEach(0..<3, id: \.self) { mode in
                        ForEach(0..<7, id: \.self) { day in
                            BarMark(x: .value("Day", days[day]),
                                    y: .value("Kilometers", kilometers[mode][day]))
                                .foregroundStyle(by: .value("Mode", modes[mode]))
                                .position(by: .value("Mode", modes[mode]))
                                .cornerRadius(3)
                        }
                    }
                }
                .chartForegroundStyleScale(domain: modes, range: modeColors)
                .chartYAxis(.hidden)
                .frame(height: 150)
            }
            card("Fares to Lisbon", "from €141") {
                Chart {
                    ForEach(0..<7, id: \.self) { day in
                        AreaMark(x: .value("Day", day),
                                 yStart: .value("Floor", 120.0),
                                 yEnd: .value("Fare", fares[0][day]))
                            .foregroundStyle(LinearGradient(
                                colors: [cityColors[0].opacity(0.35), .clear],
                                startPoint: .top, endPoint: .bottom))
                    }
                    ForEach(0..<3, id: \.self) { city in
                        ForEach(0..<7, id: \.self) { day in
                            LineMark(x: .value("Day", day), y: .value("Fare", fares[city][day]))
                                .foregroundStyle(by: .value("From", cities[city]))
                                .lineStyle(StrokeStyle(lineWidth: 2.5, lineCap: .round))
                        }
                    }
                }
                .chartForegroundStyleScale(domain: cities, range: cityColors)
                .chartXAxis(.hidden)
                .chartYAxis(.hidden)
                .chartYScale(domain: 120.0...260.0)
                .frame(height: 110)
            }
            card("Trip budget", "€1,220") {
                HStack(spacing: 20) {
                    Chart(0..<3, id: \.self) { index in
                        SectorMark(angle: .value("Euros", amounts[index]),
                                   innerRadius: .ratio(0.66), angularInset: 1.5)
                            .foregroundStyle(budgetColors[index])
                    }
                    .frame(width: 88, height: 88)
                    VStack(alignment: .leading, spacing: 6) {
                        ForEach(0..<3, id: \.self) { index in
                            HStack(spacing: 6) {
                                Circle().fill(budgetColors[index]).frame(width: 8, height: 8)
                                Text("\(names[index]) €\(Int(amounts[index]))")
                                    .font(.caption.weight(.semibold))
                                    .foregroundStyle(Color.foregroundSecondary)
                            }
                        }
                    }
                }
            }
        }
    }

    func card<Content: View>(_ title: String, _ value: String,
                             @ViewBuilder content: () -> Content) -> some View {
        VStack(alignment: .leading, spacing: 10) {
            HStack {
                Text(title).font(.footnote.weight(.semibold))
                    .foregroundStyle(Color.foregroundSecondary)
                Spacer()
                Text(value).font(.headline).foregroundStyle(Color.foregroundPrimary)
            }
            content()
        }
        .padding(14)
        .background(Color.backgroundSecondary, in: .rect(cornerRadius: 16))
    }
}

TripCharts(theme: Color.proViolet)
```

What this draws:

| Color                      | light mode                             | dark mode                              |
| -------------------------- | -------------------------------------- | -------------------------------------- |
| `gray.backgroundSecondary` | #f4f5f5                                | #2c2c2c                                |
| `gray.foregroundSecondary` | #2c2c2c                                | #dddddd                                |
| `gray.foregroundPrimary`   | #000001                                | #ffffff                                |
| `violet._500`              | #907de4                                | #907de4                                |
| `orange._500`              | #d6702d                                | #d6702d                                |
| `emerald._500`             | color(display-p3 0.1667 0.6245 0.4817) | color(display-p3 0.1667 0.6245 0.4817) |
| `gray.foregroundTertiary`  | #454545                                | #c0c0c0                                |
| `pink._500`                | #d9668e                                | #d9668e                                |
| `indigo._500`              | #6b86e9                                | #6b86e9                                |
| `gold._500`                | color(display-p3 0.7045 0.5142 0.0538) | color(display-p3 0.7045 0.5142 0.0538) |
| `blue._500`                | #3690e2                                | #3690e2                                |
| `coral._500`               | #db6c44                                | #db6c44                                |
| `grass._500`               | #519d3f                                | #519d3f                                |

## Next steps

- [Setting up themes](https://colortokenskit.com/advanced/themes/index.md): Recolor a view, a screen or your whole app from one value, with the same contrast in every hue.
- [Building for interaction states](https://colortokenskit.com/getting-started/interaction-states/index.md): Give buttons and rows hover, pressed, selected and disabled colors that stay readable in both modes.
- [Understanding semantic tokens](https://colortokenskit.com/getting-started/semantic-tokens/index.md): Learn the 20 tokens by the job each one does, and get dark mode and passing contrast with them.

---

From ColorTokensKit, by Penguin Design Ventures: https://colortokenskit.com/getting-started/components/ (updated September 26, 2026).
