# Replacing your app colors

Move backgrounds, text, outlines, states, gradients and charts to tokens, one screen at a time.

Swap each hard-coded or asset-catalog color for the [semantic token](https://colortokenskit.com/getting-started/semantic-tokens/index.md) that does its job, and it gets a dark-mode color and a contrast you can count on. You can move one screen at a time: search for `Color("`, `Color(red:`, `Color(white:`, `UIColor(named:` and `NSColor(named:`, then work through what you find, kind by kind, in the order below.

Use the gray tokens on `Color` for everyday backgrounds, text and borders, and your brand's own tokens where a color carries the brand.

## Turn your brand color into a family

Start with your brand, so the rest of the swap has a family to use. Make one from your brand's hex value:

- \#00B386

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

Text("Your flight to Kyoto boards at 14:30.")
    .foregroundStyle(Color.brand.foregroundPrimary)   // _1000 in light mode, _50 in dark
    .padding()
    .frame(maxWidth: .infinity, alignment: .leading)
    .background(Color.brand.backgroundPrimary)        // _50 in light mode, _1000 in dark
```

What this draws, with `#00B386`:

| Color               | light mode                            | dark mode                             |
| ------------------- | ------------------------------------- | ------------------------------------- |
| `backgroundPrimary` | #eaf8f2                               | color(display-p3 0.0176 0.1298 0.086) |
| `foregroundPrimary` | color(display-p3 0.0176 0.1298 0.086) | #eaf8f2                               |

The family has all 20 stops and every token, with the same contrast as the built-in themes. Your hex lands between its `_400` and `_450`, and `Color.brand.toColor()` is still exactly `#00B386` for places like a logo. Keep the exact hex out of text: on white it measures 2.69:1, while the family's text tokens pass AA in both modes. [`ProTheme(hex:)`](https://colortokenskit.com/api/protheme/index.md#from-a-hex-color) covers every way to read a hex.

## Replacing backgrounds

Backgrounds go by depth: `backgroundPrimary` for the screen, `backgroundSecondary` for cards and grouped sections, and `backgroundTertiary` for what sits on a card. A layer that lets content show through, like a sheet over a photo, takes a surface token.

```swift
VStack(alignment: .leading, spacing: 8) {
    Text("Upcoming trips")
        .font(.headline)
    Text("Lisbon, October 12")
        .padding(12)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(Color.backgroundTertiary, in: .rect(cornerRadius: 10))   // was white: 0.9
}
.foregroundStyle(Color.foregroundPrimary)
.padding()
.background(Color.backgroundSecondary, in: .rect(cornerRadius: 16))   // was Color(white: 0.96)
```

What this draws, with `Color`:

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

[Understanding semantic tokens](https://colortokenskit.com/getting-started/semantic-tokens/index.md) shows which token paints which part of a screen.

## Replacing text colors

Text goes by importance: `foregroundPrimary` for titles and body text, where you had `.primary` or `.black`; `foregroundSecondary` for subtitles, where you had `.secondary` or a mid gray; and `foregroundTertiary` for hints and captions. Every text token reaches 4.5:1 on `backgroundPrimary` and `backgroundSecondary`, in both modes and in every family.

```swift
VStack(alignment: .leading, spacing: 4) {
    Text("Lisbon")
        .font(.headline)
        .foregroundStyle(Color.foregroundPrimary)     // was .black
    Text("3 nights from October 12")
        .font(.subheadline)
        .foregroundStyle(Color.foregroundSecondary)   // was Color(white: 0.4)
}
```

What this draws, with `Color`:

| Color                 | light mode | dark mode |
| --------------------- | ---------- | --------- |
| `foregroundPrimary`   | #000001    | #ffffff   |
| `foregroundSecondary` | #2c2c2c    | #dddddd   |

An extra shade, such as a SubtitleMuted, comes from one token with [`soften(by:)`](https://colortokenskit.com/api/soften/index.md) or [`strengthen(by:)`](https://colortokenskit.com/api/strengthen/index.md) instead of a new color. Text on a filled button takes the inverted tokens, shown under buttons below.

## Replacing outlines and dividers

A control's edge, like a text field or a checkbox, takes `outlinePrimary`, which reaches 3:1 on every background token in both modes, as WCAG asks for edges people need to see. Dividers and other decorative lines take `outlineSecondary` or `outlineTertiary`, which stay quiet.

```swift
VStack(alignment: .leading, spacing: 12) {
    TextField("City or airport", text: .constant(""))
        .padding(12)
        .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.outlinePrimary))   // was gray
    Divider()
        .overlay(Color.outlineTertiary)   // was .gray.opacity(0.2)
    Text("Recent searches")
        .foregroundStyle(Color.foregroundSecondary)
}
```

What this draws, with `Color`:

| Color                 | light mode | dark mode |
| --------------------- | ---------- | --------- |
| `foregroundTertiary`  | #454545    | #c0c0c0   |
| `outlinePrimary`      | #626262    | #b1b1b1   |
| `outlineTertiary`     | #f4f5f5    | #171718   |
| `foregroundSecondary` | #2c2c2c    | #dddddd   |

[Borders people need to see](https://colortokenskit.com/advanced/color-theory/index.md#borders-people-need-to-see) has the numbers for each background.

## Replacing button and state colors

A filled button takes a theme's inverted tokens: `invertedBackgroundTertiary` behind `invertedForegroundPrimary`. Instead of more color sets for pressed and disabled, derive them from the fill: [`darken(by:)`](https://colortokenskit.com/api/darken/index.md) lands one stop darker, and [`desaturate(by: 1)`](https://colortokenskit.com/api/desaturate/index.md) turns it gray at the same lightness, so the label keeps its contrast.

```swift
let fill = Color.proIndigo.invertedBackgroundTertiary   // was Color("ButtonBlue")

HStack {
    Text("Book").padding(12).background(fill, in: Capsule())
    Text("Pressed").padding(12).background(fill.darken(by: 1), in: Capsule())
    Text("Sold out").padding(12).background(fill.desaturate(by: 1), in: Capsule())
}
.foregroundStyle(Color.proIndigo.invertedForegroundPrimary)
```

What this draws, with `Color.proIndigo`:

| Color                        | light mode                             | dark mode                              |
| ---------------------------- | -------------------------------------- | -------------------------------------- |
| `invertedForegroundPrimary`  | color(display-p3 0.9477 0.9599 0.9992) | #131a35                                |
| `invertedBackgroundTertiary` | #4f64af                                | color(display-p3 0.7152 0.7771 0.9957) |
| `_700/_300`                  | #45599d                                | color(display-p3 0.6542 0.727 0.9948)  |
| `#686868/#C8C8C8`            | #686868                                | #c8c8c8                                |

[Building for interaction states](https://colortokenskit.com/getting-started/interaction-states/index.md) puts hover, pressed, selected and disabled in one button style.

## Replacing gradients

A hand-built `LinearGradient` between two asset colors becomes two tokens and [`proGradient()`](https://colortokenskit.com/api/pro-gradient/index.md). It blends through OKLCH, so the middle stays vivid instead of going gray, and token ends switch with dark mode.

```swift
let sunset = Color.proOrange
// was LinearGradient(colors: [Color("Peach"), Color("Apricot")], startPoint: .top, …)
let colors: [Color] = [sunset.backgroundSecondary, sunset.backgroundTertiary]

RoundedRectangle(cornerRadius: 16)
    .fill(colors.proGradient())
    .frame(height: 80)
```

What this draws, with `Color.proOrange`:

| Color                 | light mode                             | dark mode |
| --------------------- | -------------------------------------- | --------- |
| `backgroundSecondary` | color(display-p3 0.9885 0.9007 0.8455) | #6e3612   |
| `backgroundTertiary`  | color(display-p3 0.978 0.79 0.6716)    | #90491a   |

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

[Gradient theory](https://colortokenskit.com/advanced/gradient-theory/index.md) explains the blends and recipes.

## Replacing chart colors

A chart's hand-picked series colors become one family per series. Spread them around the wheel with a harmony such as [`.triad`](https://colortokenskit.com/api/triad/index.md), so neighbors stay apart, and give each series a second cue, like a label or a pattern, for readers who can't tell hues apart. [Using tokens in charts](https://colortokenskit.com/advanced/charts/index.md) shows which stops to use on each background.

## Fixed colors and the asset catalog

Some colors shouldn't switch with dark mode. A brand fill that looks the same in both, like a green primary button, takes a fixed [stop](https://colortokenskit.com/api/stops/index.md): white text on `_600` measures 4.68:1, and black text on `_400` 8.46:1, in every hue. A logo takes your exact hex, `Color.brand.toColor()`.

Everything else can leave the asset catalog. Check your storyboards too, which name color sets as text; the [UIKit](https://colortokenskit.com/platforms/uikit/index.md) and [AppKit](https://colortokenskit.com/platforms/appkit/index.md) pages show tokens in those views. Then delete the color sets from `Assets.xcassets`, but keep `AccentColor` if your target's **Global Accent Color Name** build setting points to it.

## Check your work

- **Dark mode.** Switch the appearance on each screen you moved. A color that doesn't change is a stop, such as `Color.proGray._700.toColor()`, where a token belongs, so use the token for the job. [Managing dark mode](https://colortokenskit.com/getting-started/dark-mode/index.md) explains the difference.
- **Contrast.** Measure the pairs you built yourself with [`contrastRatio(to:method:)`](https://colortokenskit.com/api/contrast-ratio/index.md), in a unit test if you like.
- **High contrast.** Tokens don't follow the Increase Contrast setting on their own. [High contrast modes](https://colortokenskit.com/advanced/high-contrast/index.md) shows the one rule to add.
- **A gray brand color.** If it came out mustard, it was built from its hue with `.primary(forHue:)`. A gray has no real hue, and `#333333` reads as 89.88°, so build it with `ProTheme(hex: "#333333")`, which sees the gray, gives you the gray family and keeps your exact hex.

## Next steps

- [Managing dark mode](https://colortokenskit.com/getting-started/dark-mode/index.md): Get dark mode without extra code: tokens switch stops for you, and your own colors can too.
- [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.
- [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.
- [ProTheme](https://colortokenskit.com/api/protheme/index.md): Make a theme from a hex, a hue or OKLCH numbers, and every stop and token keeps its contrast.

---

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