# .proGradient()

Fill cards and buttons with gradients that stay vivid end to end, in light and dark mode.

Fill a card, header or button with a gradient that stays vivid from end to end: call `proGradient()` on two or more colors. You get SwiftUI's own `LinearGradient`, top to bottom unless you pass other points, 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`.

The lab draws `proGradient` next to SwiftUI's own gradient and a plain RGB blend. Change the colors, blend or easing and watch the middle:

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/api/pro-gradient/).

## 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`.

## Choosing a blend

`blend` sets the path between two colors. `.vivid`, the default, goes around the OKLCH hue wheel the short way, so sea to sand passes teal and green. `.direct` cuts straight across, the way SwiftUI's own gradient does on iOS, and meets in a paler middle. `.rainbow` goes around the long way:

```swift
let sea = Color.proBlue._600
let sand = Color.proYellow._300

VStack(spacing: 0) {
    Rectangle().fill([sea, sand].proGradient(from: .leading, to: .trailing))
    Rectangle().fill([sea, sand].proGradient(from: .leading, to: .trailing, blend: .direct))
    Rectangle().fill([sea, sand].proGradient(from: .leading, to: .trailing, blend: .rainbow))
}
.frame(height: 120)
```

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`; a linear gradient from `_600` to `yellow._300`, with `.direct` and `.smooth`; a linear gradient from `_600` to `yellow._300`, with `.rainbow` and `.smooth`.

## Easing where the colors change

`easing` moves where along the view the colors change. The default, `.smooth`, starts and finishes gently, so the gradient has no hard edge where it meets the views around it. `.linear` changes at a constant rate, and `.timingCurve` takes your own curve, as SwiftUI's `Animation.timingCurve` does:

```swift
let lisbon = Color.proOrange

VStack(spacing: 0) {
    Rectangle().fill([lisbon._600, lisbon._100].proGradient(from: .leading, to: .trailing, easing: .linear))
    Rectangle().fill([lisbon._600, lisbon._100].proGradient(from: .leading, to: .trailing, easing: .timingCurve(0.8, 0, 0.2, 1)))
}
.frame(height: 80)
```

What this draws, with `Color.proOrange`:

| Color  | light mode                             | dark mode                              |
| ------ | -------------------------------------- | -------------------------------------- |
| `_600` | #b35c24                                | #b35c24                                |
| `_100` | color(display-p3 0.9885 0.9007 0.8455) | color(display-p3 0.9885 0.9007 0.8455) |

It draws a linear gradient from `_600` to `_100`, with `.vivid` and `.linear`; a linear gradient from `_600` to `_100`, with `.vivid` and `.timingCurve(0.8, 0, 0.2, 1)`.

The custom curve holds each end's color longer and changes quickly in the middle. [How gradients blend and ease](https://colortokenskit.com/under-the-hood/gradient-blends-and-easing/index.md) draws every preset curve.

## Three or more colors

Pass as many colors as you like, in order. A banner for all three destinations runs through Lisbon's orange, Kyoto's pink and Reykjavík's sky blue:

```swift
Rectangle()
    .fill([Color.proOrange, Color.proPink, Color.proSky].proGradient(from: .leading, to: .trailing))
    .frame(height: 64)
```

What this draws:

| Color         | light mode                             | dark mode                              |
| ------------- | -------------------------------------- | -------------------------------------- |
| `orange._450` | #e97a31                                | #e97a31                                |
| `pink._450`   | #eb6f9a                                | #eb6f9a                                |
| `sky._450`    | color(display-p3 0.1688 0.6353 0.8712) | color(display-p3 0.1688 0.6353 0.8712) |

It draws a linear gradient from `orange._450` to `pink._450` to `sky._450`, with `.vivid` and `.smooth`.

A family in the array stands for its own color: `_450` for a built-in family, and your hex for a family made with [`ProTheme(hex:)`](https://colortokenskit.com/api/protheme/index.md#from-a-hex-color). A [harmony](https://colortokenskit.com/api/harmony/index.md) is an array of families too, so `brand.triad.proGradient()` works as it is.

## How it behaves

The in-between colors are worked out when SwiftUI draws the gradient, in the current appearance, so a gradient between tokens stays right in light and dark mode. SwiftUI 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.

A gray end borrows the other end's hue, so gray to blue doesn't sweep through other hues on the way. Fading to `.clear` fades only the opacity: the clear end takes the other end's color, so the fade doesn't darken on the way. Where a path leaves Display P3, the library [fits the color back in](https://colortokenskit.com/under-the-hood/gamut-mapping/index.md), lowering its chroma at the same lightness and hue.

One color gives a solid fill, and an empty array gives a gradient with no stops. `.timingCurve` holds its four values to 0 to 1, since a gradient can't overshoot its colors.

Called on one color instead of an array, `proGradient` makes the other colors with a recipe; [ProGradient recipes](https://colortokenskit.com/api/gradient-recipes/index.md) covers those.

## API

`proGradient` is an extension on arrays of `Color` and `ProTheme`, and on `Color` and `ProTheme` themselves.

```swift
public extension Array where Element == Color {
    func proGradient(from start: UnitPoint = .top, to end: UnitPoint = .bottom,
                     blend: ProGradient.Blend = .vivid, easing: ProGradient.Easing = .smooth) -> LinearGradient
}
// The same function exists on Array where Element == ProTheme.

public extension Color {
    func proGradient(_ recipe: ProGradient.Recipe = .subtle,
                     from start: UnitPoint = .top, to end: UnitPoint = .bottom,
                     blend: ProGradient.Blend = .vivid, easing: ProGradient.Easing = .smooth) -> LinearGradient
}
// The same function exists on ProTheme.

public enum ProGradient {
    public enum Blend: Hashable, Sendable {
        case vivid     // around the OKLCH hue wheel, the short way (default)
        case direct    // a straight line through OKLab
        case rainbow   // around the hue wheel, the long way
    }

    public struct Easing: Hashable, Sendable {
        public static let smooth: Easing      // cubic-bezier(0.37, 0, 0.63, 1), the default
        public static let linear: Easing
        public static let easeIn: Easing      // (0.42, 0, 1, 1)
        public static let easeOut: Easing     // (0, 0, 0.58, 1)
        public static let easeInOut: Easing   // (0.42, 0, 0.58, 1)
        public static func timingCurve(_ x1: Double, _ y1: Double, _ x2: Double, _ y2: Double) -> Easing
    }
}
```

| Parameter      | Type                 | Default           | What other values do                                                                                            |
| -------------- | -------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------- |
| `start`, `end` | `UnitPoint`          | `.top`, `.bottom` | Where the gradient starts and ends, such as `.leading` and `.trailing`.                                         |
| `blend`        | `ProGradient.Blend`  | `.vivid`          | `.direct` goes straight through OKLab; `.rainbow` goes around the hue wheel the long way.                       |
| `easing`       | `ProGradient.Easing` | `.smooth`         | Another preset or `.timingCurve(_:_:_:_:)` moves where the colors change.                                       |
| `recipe`       | `ProGradient.Recipe` | `.subtle`         | One-color version only: another [recipe](https://colortokenskit.com/api/gradient-recipes/index.md) or your own. |

It returns SwiftUI's `LinearGradient`, so it works anywhere a shape style does, such as `.background`, `.fill`, `.stroke` and `.foregroundStyle`.

## Sources

- ColorTokensKit source: [Array+ProGradient.swift](https://github.com/metasidd/ColorTokensKit-Swift/blob/main/Sources/ColorTokensKit/Gradients/Array+ProGradient.swift), [ProGradient.swift](https://github.com/metasidd/ColorTokensKit-Swift/blob/main/Sources/ColorTokensKit/Gradients/ProGradient.swift) and [GradientStops.swift](https://github.com/metasidd/ColorTokensKit-Swift/blob/main/Sources/ColorTokensKit/Gradients/GradientStops.swift).
- Apple, [LinearGradient](https://developer.apple.com/documentation/swiftui/lineargradient).

## See also

- [Gradient theory](https://colortokenskit.com/advanced/gradient-theory/index.md): Add depth with gradients that stay vivid, from a single color or a recipe, in both modes.
- [.proRadialGradient()](https://colortokenskit.com/api/pro-radial-gradient/index.md): Draw glows and spotlights that fade from the center outward without going gray.
- [.proAngularGradient()](https://colortokenskit.com/api/pro-angular-gradient/index.md): Draw rings and dials that sweep through your colors, with no seam where the ends meet.
- [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.

---

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