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:
Rectangle()
.fill([Color.proBlue._600, Color.proYellow._300].proGradient(from: .leading, to: .trailing))
.frame(height: 96)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:
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
[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:
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)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:
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)
}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:
Color.proBlue._400.toColor().proGradient(.fade)
| 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:
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 _500Writing 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:
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)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:
Circle()
.strokeBorder(brand._400.triad.proAngularGradient(), lineWidth: 8)
.frame(width: 80, height: 80)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
- Managing dark mode Get dark mode without extra code: tokens switch stops for you, and your own colors can too.
- Fitting colors into Display P3 Stay inside Display P3 without losing contrast: chroma drops, lightness holds.
- Understanding semantic tokens Learn the 20 tokens by the job each one does, and get dark mode and passing contrast with them.
- .blend(with:by:) Mix two colors into one that follows dark mode, or fade a color out cleanly.