# Gradient theory

Add depth with gradients that stay vivid, from a single color or a recipe, in both modes.

A gradient between two colors goes gray in the middle when it cuts straight across the color wheel, and stays vivid when it goes around it. Use that to add depth to cards, headers and buttons. Call [`proGradient()`](https://colortokenskit.com/api/pro-gradient/index.md) on two or more colors, such as sea to sand for a beach trip, and you get SwiftUI's own `LinearGradient` with a middle that doesn't turn gray:

```swift
Rectangle()
    .fill([Color.proBlue._600, Color.proYellow._300].proGradient(from: .leading, to: .trailing))
    .frame(height: 96)
```

What this draws, with `Color.proBlue`:

| Color         | light mode | dark mode |
| ------------- | ---------- | --------- |
| `_600`        | #2c77bd    | #2c77bd   |
| `yellow._300` | #d0bd4b    | #d0bd4b   |

It draws a linear gradient from `_600` to `yellow._300`, with `.vivid` and `.smooth`.

Halfway across, `proGradient` gives a teal as vivid as its ends, at [chroma](https://colortokenskit.com/reference/glossary/index.md#chroma) 0.13, where SwiftUI's own gradient on iOS gives a grayish teal at 0.03. The lab draws both, and an RGB blend. Change the colors, blend, easing or shape to compare:

A gradient lab builds a proGradient from colors, a blend, an easing and a shape you pick, next to SwiftUI's own gradient and a blend in RGB. By default, `Color.proBlue._600` to `Color.proYellow._300` with `.vivid` and `.smooth` makes 18 stops. [Try it on the live page](https://colortokenskit.com/advanced/gradient-theory/).

## Depth on a card in both modes

A gradient between two tokens follows dark mode the way the tokens do:

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

VStack(alignment: .leading, spacing: 4) {
    Text("Lisbon")
        .font(.headline)
        .foregroundStyle(theme.foregroundPrimary)
    Text("3 nights from October 12")
        .font(.subheadline)
        .foregroundStyle(theme.foregroundSecondary)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background([theme.backgroundSecondary, theme.backgroundTertiary].proGradient())   // _100 to _200 in light mode, _800 to _700 in dark
.border(theme.outlineSecondary)
```

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                                |
| `outlineSecondary`    | color(display-p3 0.978 0.79 0.6716)    | #6e3612                                |
| `foregroundPrimary`   | #301404                                | color(display-p3 0.9936 0.9528 0.9271) |
| `foregroundSecondary` | #6e3612                                | color(display-p3 0.978 0.79 0.6716)    |

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

## A gradient from one color

On one color, `proGradient()` makes the other colors with a [recipe](https://colortokenskit.com/api/gradient-recipes/index.md): `.subtle` unless you name another, which runs from one stop lighter at the top to the color itself:

```swift
HStack {
    Text("Book now")
        .padding()
        .foregroundStyle(.black)
        .background(brand._400.proGradient())                  // _350 at the top to _400
    Circle()
        .fill(Color.proPink._400.proRadialGradient(.fade))   // pink, fading to clear
        .frame(width: 80, height: 80)
}
```

What this draws, with `#00B386`:

| Color       | light mode                             | dark mode                              |
| ----------- | -------------------------------------- | -------------------------------------- |
| `_400`      | color(display-p3 0.2666 0.7286 0.5502) | color(display-p3 0.2666 0.7286 0.5502) |
| `black`     | #000000                                | #000000                                |
| `pink._400` | #fa7ba7                                | #fa7ba7                                |

It draws a linear gradient from the `.subtle` recipe from `_400`, with `.vivid` and `.smooth`; a radial gradient from the `.fade` recipe from `pink._400`, with `.vivid` and `.smooth`.

Both ends are palette stops, so the black label keeps their contrast: 9.73:1 at the top and 8.46:1 at the bottom.

## Choosing a recipe

Pick a color or a token below to draw every recipe from it, in both modes. The checkerboard shows where a recipe turns transparent:

The live page shows all seven recipes made from one color. For `Color.proBlue._400`, `.subtle` runs from #7ab4fd to #63a7fd, and `.tonal` runs from #8fc0fe through #63a7fd to #508edc. [Try it on the live page](https://colortokenskit.com/advanced/gradient-theory/).

| Recipe              | Colors it makes from `color`                                 | Good for                 |
| ------------------- | ------------------------------------------------------------ | ------------------------ |
| `.subtle` (default) | `color.lighten()`, `color`                                   | Buttons and icons        |
| `.fade`             | `color`, `color.opacity(0)`                                  | Glows and scrims         |
| `.tonal`            | `color.lighten(by: 2)`, `color`, `color.darken(by: 2)`       | Cards and headers        |
| `.analogous`        | `color.analogous()`, the hues 30° either side                | Banners                  |
| `.wash`             | `color.opacity(0.1)`, `color.opacity(0.3)`                   | Card backdrops           |
| `.sheen`            | `color.opacity(0)`, `color.opacity(0.5)`, `color.opacity(0)` | A shine, made from white |
| `.edgeHighlight`    | `color.opacity(0.15)`, `color`, `color.opacity(0.15)`        | Borders and rims         |

### Adding a sheen to a button

`.sheen` puts a band of color across the middle, clear at both ends. Made from white, it reads as a shine:

```swift
Button("Book now") {}
    .buttonStyle(.plain)
    .padding()
    .foregroundStyle(.black)
    .background(Color.white.proGradient(.sheen, from: .leading, to: .trailing))
    .background(brand._400.proGradient(.tonal))   // _300 through _400 to _500
```

What this draws, with `#00B386`:

| Color   | light mode                             | dark mode                              |
| ------- | -------------------------------------- | -------------------------------------- |
| `_400`  | color(display-p3 0.2666 0.7286 0.5502) | color(display-p3 0.2666 0.7286 0.5502) |
| `white` | #ffffff                                | #ffffff                                |
| `black` | #000000                                | #000000                                |

It draws a linear gradient from the `.tonal` recipe from `_400`, with `.vivid` and `.smooth`; a linear gradient from the `.sheen` recipe from `white`, with `.vivid` and `.smooth`.

### Writing your own recipe

A recipe is a function from one `Color` to the gradient's colors, in order. Add it as a static property, and it reads like the presets:

```swift
extension ProGradient.Recipe {
    static var deepen: Self { Self { [$0, $0.darken(by: 4)] } }
}

Rectangle()
    .fill(brand._400.proGradient(.deepen))   // _400 to _600
    .frame(height: 64)
```

What this draws, with `#00B386`:

| Color  | light mode                             | dark mode                              |
| ------ | -------------------------------------- | -------------------------------------- |
| `_400` | color(display-p3 0.2666 0.7286 0.5502) | color(display-p3 0.2666 0.7286 0.5502) |
| `_600` | color(display-p3 0.1547 0.5182 0.3824) | color(display-p3 0.1547 0.5182 0.3824) |

It draws a linear gradient from `_400` to `_600`, with `.vivid` and `.smooth`.

## A ring from a harmony

A [harmony](https://colortokenskit.com/advanced/color-theory/index.md#other-hues-that-go-with-yours) is an array of families, so it makes a gradient directly. [`proAngularGradient`](https://colortokenskit.com/api/pro-angular-gradient/index.md) adds your first color again at the end, so the ring has no seam:

```swift
Circle()
    .strokeBorder(brand._400.triad.proAngularGradient(), lineWidth: 8)
    .frame(width: 80, height: 80)
```

What this draws, with `#00B386`:

| Color             | light mode                             | dark mode                              |
| ----------------- | -------------------------------------- | -------------------------------------- |
| `_400`            | color(display-p3 0.2666 0.7286 0.5502) | color(display-p3 0.2666 0.7286 0.5502) |
| `hue:286.99._400` | color(display-p3 0.63 0.5917 0.9929)   | color(display-p3 0.63 0.5917 0.9929)   |
| `hue:46.99._400`  | #f98546                                | #f98546                                |

It draws a angular gradient from `_400` to `hue:286.99._400` to `hue:46.99._400`, with `.vivid` and `.smooth`.

## How it behaves

A `proGradient` stays vivid because it goes around the OKLCH hue wheel, where SwiftUI's own gradient cuts across it. [How gradients blend and ease](https://colortokenskit.com/under-the-hood/gradient-blends-and-easing/index.md) explains why, with the other blends and easing curves. SwiftUI still blends between neighboring stops its own way, so `proGradient` adds up to 32 stops per pair of colors, close enough that its blending can't show.

Recipes built on [`lighten()`](https://colortokenskit.com/api/lighten/index.md) and [`darken()`](https://colortokenskit.com/api/darken/index.md) land palette colors on exact stops. On a family, a recipe starts from the family's own color: `_450` for a built-in family, and your hex for a brand family. To start from a stop, call it on the stop.

## The functions

Each function has its own page with its signature and options: [`proGradient()`](https://colortokenskit.com/api/pro-gradient/index.md) for a linear gradient, [`proRadialGradient()`](https://colortokenskit.com/api/pro-radial-gradient/index.md) and [`proAngularGradient()`](https://colortokenskit.com/api/pro-angular-gradient/index.md) for the other two shapes, and [ProGradient recipes](https://colortokenskit.com/api/gradient-recipes/index.md) for the presets and your own.

## See also

- [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.
- [Fitting colors into Display P3](https://colortokenskit.com/under-the-hood/gamut-mapping/index.md): Stay inside Display P3 without losing contrast: chroma drops, lightness holds.
- [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.
- [.blend(with:by:)](https://colortokenskit.com/api/blend/index.md): Mix two colors into one that follows dark mode, or fade a color out cleanly.

---

From ColorTokensKit, by Penguin Design Ventures: https://colortokenskit.com/advanced/gradient-theory/ (updated September 26, 2026).
