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.
Rectangle()
.fill([Color.proBlue._600, Color.proYellow._300].proGradient(from: .leading, to: .trailing))
.frame(height: 96)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:
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)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:
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)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:
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)The custom curve holds each end's color longer and changes quickly in the middle. How gradients blend and ease 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:
Rectangle()
.fill([Color.proOrange, Color.proPink, Color.proSky].proGradient(from: .leading, to: .trailing))
.frame(height: 64)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:). A harmony 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, 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 covers those.
API#
proGradient is an extension on arrays of Color and ProTheme, and on Color and ProTheme themselves.
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 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, ProGradient.swift and GradientStops.swift.
- Apple, LinearGradient.
See also
- Gradient theory Add depth with gradients that stay vivid, from a single color or a recipe, in both modes.
- .proRadialGradient() Draw glows and spotlights that fade from the center outward without going gray.
- .proAngularGradient() Draw rings and dials that sweep through your colors, with no seam where the ends meet.
- Managing dark mode Get dark mode without extra code: tokens switch stops for you, and your own colors can too.