Skip to content
ColorTokensKitby Penguin Design Ventures
Contents
How-to

Gradient theory

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

Updated View as Markdown

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() 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)
light mode

Halfway across, proGradient gives a teal as vivid as its ends, at 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:

Try it / Gradient lab
Colors
Blend
Shape

proGradient · 18 stops, worked out in OKLCH

SwiftUI's LinearGradient: a straight line through OKLab

Blended in RGB: SwiftUI's .device and CSS's default

swift
[Color.proBlue._600.toColor(), Color.proYellow._300.toColor()]
    .proGradient(from: .leading, to: .trailing)

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)
light mode
Lisbon3 nights from October 12

A gradient from one color#

On one color, proGradient() makes the other colors with a recipe: .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)
}
light mode
Book now

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:

Try it / Recipes
Color
light mode
.subtleA touch lighter at the start
.fadeThe color fading to transparent
.tonalTwo stops lighter to two stops darker
.analogousA drift through the neighboring hues
.washA soft, translucent tint
.sheenA band of color, clear at both ends
.edgeHighlightStrongest in the middle
dark mode
.subtleA touch lighter at the start
.fadeThe color fading to transparent
.tonalTwo stops lighter to two stops darker
.analogousA drift through the neighboring hues
.washA soft, translucent tint
.sheenA band of color, clear at both ends
.edgeHighlightStrongest in the middle

Color.proBlue._400.toColor().proGradient(.fade)

RecipeColors it makes from colorGood for
.subtle (default)color.lighten(), colorButtons and icons
.fadecolor, color.opacity(0)Glows and scrims
.tonalcolor.lighten(by: 2), color, color.darken(by: 2)Cards and headers
.analogouscolor.analogous(), the hues 30° either sideBanners
.washcolor.opacity(0.1), color.opacity(0.3)Card backdrops
.sheencolor.opacity(0), color.opacity(0.5), color.opacity(0)A shine, made from white
.edgeHighlightcolor.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
light mode
Book now

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)
light mode

A ring from a harmony#

A harmony is an array of families, so it makes a gradient directly. proAngularGradient 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)
light mode

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 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() and darken() 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() for a linear gradient, proRadialGradient() and proAngularGradient() for the other two shapes, and ProGradient recipes for the presets and your own.

See also