# Understanding semantic tokens

Learn the 20 tokens by the job each one does, and get dark mode and passing contrast with them.

A semantic token names the job a color does, such as a title or a card's background, instead of the color itself. Every theme has the same 20 tokens, each with one [stop](https://colortokenskit.com/basics/what-are-tokens/index.md) for light mode and one for dark, so a color picked by its job, like `theme.foregroundPrimary`, switches with dark mode and keeps its contrast in any theme.

## Four kinds of token

Here's a booking card with every part labeled by the token that colors it. Switch to dark mode and every part moves to its dark stop:

The live page draws a booking card in `Color.proRose`, with each part numbered and labeled by the colors that paint it (the fill first, then what sits on it), in light or dark mode:

| #  | Part                 | Kind       | Token                                                                           | Light mode                     | Dark mode                        |
| -- | -------------------- | ---------- | ------------------------------------------------------------------------------- | ------------------------------ | -------------------------------- |
| 1  | Title and price      | Foreground | `foregroundPrimary`                                                             | `_1000` #2c1320                | `_50` #fdf2f7                    |
| 2  | Details and host     | Foreground | `foregroundSecondary`                                                           | `_800` #65354f                 | `_200` #f8c5de                   |
| 3  | Total and labels     | Foreground | `foregroundTertiary`                                                            | `_700` #854768                 | `_300` #f4a1cc                   |
| 4  | Page                 | Background | `backgroundPrimary`                                                             | `_50` #fdf2f7                  | `_1000` #2c1320                  |
| 5  | Card                 | Background | `backgroundSecondary`                                                           | `_100` #fbe3ef                 | `_800` #65354f                   |
| 6  | Price and date boxes | Background | `backgroundPrimary`                                                             | `_50` #fdf2f7                  | `_1000` #2c1320                  |
| 7  | Rating pill          | Background | `Color.backgroundPrimary` + `Color.proYellow._350` + `Color.foregroundTertiary` | #ffffff + #c5b232 + #454545    | #000000 + #c5b232 + #c0c0c0      |
| 8  | Host avatar          | Background | `invertedBackgroundTertiary` + `invertedForegroundPrimary`                      | `_650` #945075 + `_50` #fdf2f7 | `_250` #f6b3d5 + `_1000` #2c1320 |
| 9  | Reserve button       | Background | `invertedBackgroundSecondary` + `invertedForegroundPrimary`                     | `_800` #65354f + `_50` #fdf2f7 | `_150` #f9d4e6 + `_1000` #2c1320 |
| 10 | Pill over the photo  | Surface    | `surfacePrimary`                                                                | `_200` #f8c5de at 50%          | `_700` #854768 at 50%            |
| 11 | Card edge            | Outline    | `outlineTertiary`                                                               | `_100` #fbe3ef                 | `_900` #472437                   |
| 12 | Date divider         | Outline    | `outlineTertiary`                                                               | `_100` #fbe3ef                 | `_900` #472437                   |

- **Foreground**: text and icons, from `foregroundPrimary` for titles and body text to `foregroundTertiary` for hints and labels.
- **Background**: the solid areas text sits on: the page, cards, tags.
- **Surface**: see-through layers over other content, such as a pill on a photo, a sheet or a tooltip.
- **Outline**: edges and dividers. `outlinePrimary` marks a control's edge; `outlineSecondary` and `outlineTertiary` are decorative lines.

Foreground, background and surface each have an inverted twin, for an area in the opposite mode: a filled button, an avatar, or a dark banner on a light screen.

## All 20 tokens

Drag across the themes to see each token's stop and Display P3 hex in any family. Surfaces let what's behind them show through, so they're drawn over each mode's page color.

The live page shows every token of the family you drag to, grouped by kind. For `proBlue`, in Display P3 hex:

| Kind       | Token                         | Light mode            | Dark mode             | Use for                                              |
| ---------- | ----------------------------- | --------------------- | --------------------- | ---------------------------------------------------- |
| Foreground | `foregroundPrimary`           | `_1000` #0b1c32       | `_50` #eff6ff         | Primary text, selected icons                         |
| Foreground | `foregroundSecondary`         | `_800` #254771        | `_200` #b8d7fe        | Secondary text, unselected icons                     |
| Foreground | `foregroundTertiary`          | `_700` #335e94        | `_300` #8fc0fe        | Tertiary text, disabled icons                        |
| Foreground | `invertedForegroundPrimary`   | `_50` #eff6ff         | `_1000` #0b1c32       | Primary text on inverted backgrounds                 |
| Foreground | `invertedForegroundSecondary` | `_150` #cbe1fe        | `_800` #254771        | Secondary text on inverted backgrounds               |
| Foreground | `invertedForegroundTertiary`  | `_250` #a4cbfe        | `_700` #335e94        | Tertiary text on inverted backgrounds                |
| Background | `backgroundPrimary`           | `_50` #eff6ff         | `_1000` #0b1c32       | Main content and app background                      |
| Background | `backgroundSecondary`         | `_100` #ddecff        | `_800` #254771        | Secondary backgrounds, cards                         |
| Background | `backgroundTertiary`          | `_200` #b8d7fe        | `_700` #335e94        | Tertiary backgrounds, modals                         |
| Background | `invertedBackgroundPrimary`   | `_900` #183150        | `_50` #eff6ff         | Main background of inverted areas                    |
| Background | `invertedBackgroundSecondary` | `_800` #254771        | `_150` #cbe1fe        | Cards on inverted areas                              |
| Background | `invertedBackgroundTertiary`  | `_650` #3a69a5        | `_250` #a4cbfe        | Modals on inverted areas                             |
| Surface    | `surfacePrimary`              | `_200` #b8d7fe at 50% | `_700` #335e94 at 50% | Cards and sheets over other content                  |
| Surface    | `surfaceSecondary`            | `_200` #b8d7fe at 30% | `_700` #335e94 at 30% | Panels and dialogs                                   |
| Surface    | `surfaceTertiary`             | `_200` #b8d7fe at 10% | `_700` #335e94 at 10% | Tooltips and popovers                                |
| Surface    | `invertedSurfacePrimary`      | `_400` #63a7fd at 40% | `_600` #4176b8 at 40% | Cards and sheets on dark areas                       |
| Surface    | `invertedSurfaceSecondary`    | `_400` #63a7fd at 20% | `_600` #4176b8 at 20% | Panels and dialogs on dark areas                     |
| Outline    | `outlinePrimary`              | `_600` #4176b8        | `_350` #7ab4fd        | Edges people need to see, like fields and checkboxes |
| Outline    | `outlineSecondary`            | `_200` #b8d7fe        | `_800` #254771        | Decorative borders and dividers                      |
| Outline    | `outlineTertiary`             | `_100` #ddecff        | `_900` #183150        | Decorative hairlines and subtle borders              |

`ColorTokens.swift` also puts the 20 names on `Color`, built on gray, for parts of your app outside any theme. `Color.backgroundPrimary` is pure white and black.

## Contrast of token pairs

A stop has the same lightness in every hue, so a pair of tokens has the same contrast in every family, including [your own](https://colortokenskit.com/getting-started/replacing-colors/index.md). Here's every pair, drawn in `proBlue`, with its ratio in each mode:

Text on backgrounds, in `Color.proBlue`:

| Token                 | On `backgroundPrimary`      | On `backgroundSecondary`   | On `backgroundTertiary`                                   |
| --------------------- | --------------------------- | -------------------------- | --------------------------------------------------------- |
| `foregroundPrimary`   | light 15.64 ✓, dark 15.64 ✓ | light 14.19 ✓, dark 8.72 ✓ | light 11.48 ✓, dark 6.10 ✓                                |
| `foregroundSecondary` | light 8.72 ✓, dark 11.48 ✓  | light 7.91 ✓, dark 6.40 ✓  | light 6.40 ✓, dark 4.48 ✓ large text only                 |
| `foregroundTertiary`  | light 6.10 ✓, dark 9.00 ✓   | light 5.53 ✓, dark 5.02 ✓  | light 4.48 ✓ large text only, dark 3.51 ✓ large text only |

Text on inverted backgrounds, in `Color.proBlue`:

| Token                         | On `invertedBackgroundPrimary` | On `invertedBackgroundSecondary` | On `invertedBackgroundTertiary`                           |
| ----------------------------- | ------------------------------ | -------------------------------- | --------------------------------------------------------- |
| `invertedForegroundPrimary`   | light 12.08 ✓, dark 15.64 ✓    | light 8.72 ✓, dark 12.80 ✓       | light 5.11 ✓, dark 10.20 ✓                                |
| `invertedForegroundSecondary` | light 9.89 ✓, dark 8.72 ✓      | light 7.14 ✓, dark 7.14 ✓        | light 4.18 ✓ large text only, dark 5.69 ✓                 |
| `invertedForegroundTertiary`  | light 7.88 ✓, dark 6.10 ✓      | light 5.69 ✓, dark 4.99 ✓        | light 3.33 ✓ large text only, dark 3.98 ✓ large text only |

Edges on backgrounds, in `Color.proBlue`:

| Token              | On `backgroundPrimary`                          | On `backgroundSecondary`                        | On `backgroundTertiary`                         |
| ------------------ | ----------------------------------------------- | ----------------------------------------------- | ----------------------------------------------- |
| `outlinePrimary`   | light 4.29 ✓, dark 7.91 ✓                       | light 3.89 ✓, dark 4.41 ✓                       | light 3.15 ✓, dark 3.08 ✓                       |
| `outlineSecondary` | light 1.36 – decorative, dark 1.79 – decorative | light 1.24 – decorative, dark 1.00 – decorative | light 1.00 – decorative, dark 1.43 – decorative |
| `outlineTertiary`  | light 1.10 – decorative, dark 1.29 – decorative | light 1.00 – decorative, dark 1.39 – decorative | light 1.24 – decorative, dark 1.98 – decorative |

✓ passes WCAG AA: 4.5:1 for text, 3:1 for edges. "Large text only" means 3:1. ✗ fails. – decorative: a line WCAG sets no minimum for.

Keep foregrounds on backgrounds and inverted foregrounds on inverted backgrounds. Crossed over, text vanishes: `foregroundPrimary` on `invertedBackgroundPrimary` is 1.00:1 in dark mode.

Gray's ramp runs from white to black, so every gray pair passes AA. Over `backgroundPrimary`, every foreground token passes AA on every surface too. Over `invertedBackgroundPrimary`, `invertedForegroundTertiary` on `invertedSurfacePrimary` falls as low as 3.49:1.

`outlinePrimary` is for edges people need to find a control, like a text field's border, so it reaches 3:1 on every background token in both modes. `outlineSecondary` and `outlineTertiary` are for dividers and card edges, lines WCAG sets no minimum for, so they stay faint. [Borders people need to see](https://colortokenskit.com/advanced/color-theory/index.md#borders-people-need-to-see) has an example.

## Adding your own tokens

The tokens live in [ColorTokens.swift](https://github.com/metasidd/ColorTokensKit-Swift/blob/main/Tests/ColorTokensKitTests/Marketing/Setup/ColorTokens.swift), a file you copy into your app, so you can rename them, move them or add your own. A token is a property on `ProTheme` with one stop for each mode. Keep its stops at least 12 from the background they sit on, and it passes AA in every hue by [the distance rule](https://colortokenskit.com/advanced/color-theory/index.md#the-distance-rule):

```swift
public extension ProTheme {
    /// Links in running text. Color(light:dark:) takes a Color for each mode,
    /// so each stop gets toColor().
    var accentText: Color {
        // 5.11:1 on backgroundPrimary in light mode, 7.91:1 in dark
        Color(light: _650.toColor(), dark: _350.toColor())
    }
}

Text("See all trips")
    .foregroundStyle(brand.accentText)
    .padding()
    .background(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)  |
| `_650/_350`         | color(display-p3 0.1368 0.4655 0.3425) | color(display-p3 0.3144 0.7788 0.5953) |

## Next steps

- [Using your first tokens](https://colortokenskit.com/getting-started/first-tokens/index.md): Color text, buttons, cards and lists with tokens, then theme a movie card in one line.
- [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.
- [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.
- [.contrastRatio(to:method:)](https://colortokenskit.com/api/contrast-ratio/index.md): Check WCAG 2 and APCA contrast in Swift, and keep your color pairs passing with a unit test.

---

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