# Managing dark mode

Get dark mode without extra code: tokens switch stops for you, and your own colors can too.

Managing dark mode mostly means letting the tokens do it: you get dark mode without writing any dark-mode code. Each [semantic token](https://colortokenskit.com/getting-started/semantic-tokens/index.md) holds two stops, one for light mode and one for dark, and SwiftUI draws the one for the current mode:

```swift
let theme = Color.proOrange

VStack(alignment: .leading, spacing: 4) {
    Text("Lisbon")
        .font(.headline)
        .foregroundStyle(theme.foregroundPrimary)     // _1000 in light mode, _50 in dark
    Text("3 nights from October 12")
        .font(.subheadline)
        .foregroundStyle(theme.foregroundSecondary)   // _800 and _200
}
.padding()
.background(theme.backgroundSecondary)                // _100 and _800
.padding()
.background(theme.backgroundPrimary)                  // _50 and _1000
```

What this draws, with `Color.proOrange`:

| Color                 | light mode                             | dark mode                              |
| --------------------- | -------------------------------------- | -------------------------------------- |
| `backgroundPrimary`   | color(display-p3 0.9936 0.9528 0.9271) | #301404                                |
| `backgroundSecondary` | color(display-p3 0.9885 0.9007 0.8455) | #6e3612                                |
| `foregroundPrimary`   | #301404                                | color(display-p3 0.9936 0.9528 0.9271) |
| `foregroundSecondary` | #6e3612                                | color(display-p3 0.978 0.79 0.6716)    |

Every foreground token passes WCAG AA on the page in both modes.

## Tokens switch, stops stay fixed

A [stop](https://colortokenskit.com/basics/what-are-tokens/index.md) is one color in both modes, so where a token belongs, it fails quietly in dark mode. The first line uses a stop, the second a token:

```swift
let theme = Color.proOrange

VStack(alignment: .leading) {
    Text("3 nights from October 12")
        .foregroundStyle(theme._800.toColor())   // 8.72:1 in light mode, 1.79:1 in dark
    Text("3 nights from October 12")
        .foregroundStyle(theme.foregroundSecondary)   // 8.72:1 and 11.48:1
}
.padding()
.background(theme.backgroundPrimary)
```

What this draws, with `Color.proOrange`:

| Color                 | light mode                             | dark mode                           |
| --------------------- | -------------------------------------- | ----------------------------------- |
| `backgroundPrimary`   | color(display-p3 0.9936 0.9528 0.9271) | #301404                             |
| `_800`                | #6e3612                                | #6e3612                             |
| `foregroundSecondary` | #6e3612                                | color(display-p3 0.978 0.79 0.6716) |

Use a stop only when a color should look the same in both modes, such as a logo. A contrast check that reads a token sees only one of its stops; [`contrastRatio(to:method:)`](https://colortokenskit.com/api/contrast-ratio/index.md) shows how to check both.

## Dark backgrounds keep a tint of their hue

```swift
let theme = Color.proSky

VStack(alignment: .leading, spacing: 4) {
    Text("Reykjavík")
        .font(.headline)
        .foregroundStyle(theme.foregroundPrimary)
    Text("3 nights from October 12")
        .font(.subheadline)
        .foregroundStyle(theme.foregroundSecondary)
}
.padding()
.background(theme.backgroundPrimary)   // _1000 in dark mode, a very dark blue
.border(theme.outlineSecondary)
.padding()
.background(Color.backgroundPrimary)   // black in dark mode
```

What this draws, with `Color.proSky`:

| Color                   | light mode                             | dark mode                              |
| ----------------------- | -------------------------------------- | -------------------------------------- |
| `app.backgroundPrimary` | #ffffff                                | #000000                                |
| `backgroundPrimary`     | #eaf7fe                                | color(display-p3 0.0119 0.1193 0.1786) |
| `outlineSecondary`      | #9addfd                                | color(display-p3 0.0596 0.2924 0.4106) |
| `foregroundPrimary`     | color(display-p3 0.0119 0.1193 0.1786) | #eaf7fe                                |
| `foregroundSecondary`   | color(display-p3 0.0596 0.2924 0.4106) | #9addfd                                |

A family's backgrounds come from its own ramp, so the Reykjavík card stays blue in the dark. For a neutral page, use `Color.backgroundPrimary`, which is black. Every family's foreground tokens pass AA on the gray background tokens too. [What does equal lightness cost?](https://colortokenskit.com/under-the-hood/trade-offs/index.md) explains the tint.

## Your own light and dark pairs

`Color(light:dark:)` turns any two colors into one that switches with the mode, the way every token does:

```swift
// Color(light:dark:) takes two Colors, so each stop needs toColor().
let highlight = Color(
    light: Color.proYellow._200.toColor(),
    dark: Color.proYellow._800.toColor()
)
let ink = Color(light: Color(hex: "#1A1A2E"), dark: .white)

Text("Your flight to Kyoto boards at 14:30.")
    .foregroundStyle(ink)
    .frame(maxWidth: .infinity, alignment: .leading)
    .padding()
    .background(highlight)
```

What this draws, with `Color.proYellow`:

| Color           | light mode | dark mode                             |
| --------------- | ---------- | ------------------------------------- |
| `_200/_800`     | #dfd597    | color(display-p3 0.3108 0.2742 0.012) |
| `#1A1A2E/white` | #1a1a2e    | #ffffff                               |

The text measures 11.47:1 in light mode and 9.52:1 in dark. The result resolves when SwiftUI draws it, so it follows `.preferredColorScheme(.dark)` without your code reading `colorScheme`. UIKit and AppKit get the same initializer, as `UIColor(light:dark:)` and `NSColor(light:dark:)`.

## Color functions follow the mode

Color functions such as [`soften()`](https://colortokenskit.com/api/soften/index.md) work out their result each time a color is drawn, so a function applied to a token keeps adapting:

```swift
let subtitle = Color.proOrange.foregroundSecondary   // _800 in light mode, _200 in dark

VStack(alignment: .leading, spacing: 4) {
    Text("3 nights from October 12")
        .font(.subheadline)
        .foregroundStyle(subtitle)
    Text("Flights included")
        .font(.footnote)
        .foregroundStyle(subtitle.soften())   // _750 and _250: quieter in both modes
}
.padding()
.background(Color.proOrange.backgroundPrimary)
```

What this draws, with `Color.proOrange`:

| Color                 | light mode                             | dark mode                             |
| --------------------- | -------------------------------------- | ------------------------------------- |
| `backgroundPrimary`   | color(display-p3 0.9936 0.9528 0.9271) | #301404                               |
| `foregroundSecondary` | #6e3612                                | color(display-p3 0.978 0.79 0.6716)   |
| `_750/_250`           | #7f4016                                | color(display-p3 0.9744 0.7286 0.573) |

`soften()` moves toward the background, so it's quieter in both modes. [`lighten()`](https://colortokenskit.com/api/lighten/index.md) would give `_750` and `_150` here: quieter on a light page, louder on a dark one. Harmonies and gradients of tokens adapt too.

## Increase Contrast and watchOS

Tokens don't change with Increase Contrast; [high contrast modes](https://colortokenskit.com/advanced/high-contrast/index.md) shows how to strengthen them when it's on. Apple Watch always draws in dark mode, so tokens use their dark stops there, as [watchOS](https://colortokenskit.com/platforms/watchos/index.md) explains.

## Sources

- Apple, [UIColor init(dynamicProvider:)](https://developer.apple.com/documentation/uikit/uicolor/init\(dynamicprovider:\)) and [NSColor init(name:dynamicProvider:)](https://developer.apple.com/documentation/appkit/nscolor/init\(name:dynamicprovider:\)), the dynamic colors tokens are built on.
- Apple, [preferredColorScheme(\_:)](https://developer.apple.com/documentation/swiftui/view/preferredcolorscheme\(_:\)).
- Apple, [Human Interface Guidelines: Dark Mode](https://developer.apple.com/design/human-interface-guidelines/dark-mode).
- W3C, [WCAG 2.2: contrast minimum](https://www.w3.org/TR/WCAG22/#contrast-minimum).
- The library's [Color+Dynamic.swift and Color+Adaptive.swift](https://github.com/metasidd/ColorTokensKit-Swift/tree/main/Sources/ColorTokensKit/Platform/SwiftUI).

## Next steps

- [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.
- [Color(light:dark:)](https://colortokenskit.com/api/light-dark/index.md): Make any color switch between light and dark mode, the way every token does.
- [How accessible is it?](https://colortokenskit.com/basics/how-accessible/index.md): What the palette guarantees for contrast in every hue and mode, and what you still need to check.

---

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