MeshGradient: Building Rich, Fluid Visuals in SwiftUI
You have seen the backgrounds on Apple’s own apps — those shimmering, multi-directional color blends that no
LinearGradient or RadialGradient can reproduce. Before iOS 18, achieving that look required Core Image filters,
Metal shaders, or pre-baked image assets. Now a single SwiftUI view gets you there in a handful of lines.
This post covers the MeshGradient view from end to end: the initializer, how control points shape the color field,
color interpolation modes, animating mesh points for fluid motion, and the performance trade-offs you should know before
shipping it in production. We will not cover Metal shaders or custom ShapeStyle conformances — those deserve their own
deep dives.
Contents
- The Problem
- Understanding MeshGradient
- Building Your First Mesh
- Color Interpolation
- Animating Mesh Points
- Performance Considerations
- When to Use (and When Not To)
- Summary
The Problem
Suppose you are building a hero screen for a Pixar movie catalog app. The design calls for a rich, multi-color
background that shifts smoothly from a warm orange at the top-left to a deep blue at the bottom-right, with a purple
bloom radiating from the center. Your first instinct is to reach for a LinearGradient:
struct MovieHeroBackground: View {
var body: some View {
LinearGradient(
colors: [.orange, .purple, .blue],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
}
}
The result is flat. The gradient moves in a single direction, producing a band of purple that stretches uniformly across
the diagonal. There is no way to push that purple bloom toward the center or curve it around a focal point. Stacking a
RadialGradient on top helps marginally, but you end up fighting blending modes and opacity layers to approximate what
the designer actually drew.
The root issue is dimensionality. LinearGradient and RadialGradient define color along one axis (a line or a
radius). The design requires color control across a two-dimensional surface — a grid of anchor points, each pulling the
color field in its own direction.
That is exactly what MeshGradient provides.
Understanding MeshGradient
MeshGradient was introduced at WWDC 2024 (session
10185 — Create custom visual effects with SwiftUI) and ships
with iOS 18, macOS 15, tvOS 18, and visionOS 2.
A mesh gradient is defined by a rectangular grid of control points. Each point has two properties:
- Position — a
SIMD2<Float>in a unit coordinate space where(0, 0)is the top-left corner and(1, 1)is the bottom-right. - Color — a
Colorvalue assigned to that point.
The GPU interpolates colors between neighboring points across both axes, producing smooth, two-dimensional blends. Moving a single point shifts the color field around it, giving you precise spatial control that one-dimensional gradients cannot offer.
The key initializer looks like this:
@available(iOS 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, *)
MeshGradient(
width: Int,
height: Int,
points: [SIMD2<Float>],
colors: [Color],
background: Color = .clear,
smoothsColors: Bool = true
)
widthandheightdefine the grid dimensions. A 3x3 grid has 9 control points, a 4x4 grid has 16.pointsis a flat array ofSIMD2<Float>values, ordered row by row from top-left to bottom-right.colorsis a flat array ofColorvalues matching the same order.smoothsColorstoggles between Bezier-smoothed interpolation (true) and linear interpolation (false).
Note: The corner points should remain at
(0, 0),(1, 0),(0, 1), and(1, 1). Moving corners away from the edges can produce clipping artifacts or visible gaps where the background color bleeds through.
Building Your First Mesh
Let us build the hero background that our Pixar movie catalog actually needs — a warm-to-cool gradient with a purple bloom in the center. A 3x3 grid gives us nine control points: four corners, four edges, and one center point to anchor the bloom.
@available(iOS 18.0, *)
struct PixarHeroMesh: View {
var body: some View {
MeshGradient(
width: 3,
height: 3,
points: [
// Row 0 (top)
SIMD2(0.0, 0.0), SIMD2(0.5, 0.0), SIMD2(1.0, 0.0),
// Row 1 (middle)
SIMD2(0.0, 0.5), SIMD2(0.6, 0.4), SIMD2(1.0, 0.5),
// Row 2 (bottom)
SIMD2(0.0, 1.0), SIMD2(0.5, 1.0), SIMD2(1.0, 1.0)
],
colors: [
.orange, .yellow, .red,
.mint, .purple, .indigo,
.cyan, .blue, .teal
]
)
.ignoresSafeArea()
}
}
Notice the center point is offset to (0.6, 0.4) instead of sitting at (0.5, 0.5). That slight displacement pulls the
purple bloom toward the upper-right, creating an asymmetry that feels organic rather than mechanical. This is where mesh
gradients separate themselves from radial gradients — every point is independently movable.
Increasing Grid Resolution
A 3x3 grid is enough for broad washes of color. When your design demands tighter control — say, an isolated highlight in one quadrant while keeping the rest smooth — step up to a 4x4 grid:
@available(iOS 18.0, *)
struct HighResMovieMesh: View {
var body: some View {
MeshGradient(
width: 4,
height: 4,
points: [
// Row 0
SIMD2(0.0, 0.0), SIMD2(0.33, 0.0),
SIMD2(0.66, 0.0), SIMD2(1.0, 0.0),
// Row 1
SIMD2(0.0, 0.33), SIMD2(0.3, 0.25),
SIMD2(0.7, 0.35), SIMD2(1.0, 0.33),
// Row 2
SIMD2(0.0, 0.66), SIMD2(0.35, 0.7),
SIMD2(0.65, 0.6), SIMD2(1.0, 0.66),
// Row 3
SIMD2(0.0, 1.0), SIMD2(0.33, 1.0),
SIMD2(0.66, 1.0), SIMD2(1.0, 1.0)
],
colors: [
.black, .indigo, .indigo, .black,
.indigo, .purple, .blue, .indigo,
.indigo, .blue, .cyan, .teal,
.black, .indigo, .teal, .black
]
)
.ignoresSafeArea()
}
}
The outer ring of dark colors frames the gradient, while the interior points produce a glowing nebula effect — perfect for an “Inside Out” emotions dashboard or a space-themed background for a Lightyear fan app.
Tip: When designing meshes, sketch the grid on paper first. Label each point with its intended color and rough position. This saves significant trial-and-error in code.
Color Interpolation
The smoothsColors parameter controls how colors blend between control points.
When smoothsColors is true (the default), the gradient uses Bezier-curve interpolation. Colors transition with
gentle, organic curves, producing the fluid, lava-lamp aesthetic that makes mesh gradients visually distinctive. This is
the mode you want for hero backgrounds, ambient UI, and any context where the gradient should feel alive.
When smoothsColors is set to false, the gradient switches to linear interpolation. Colors blend in straight lines
between control points, producing sharper boundaries and a more geometric look. This can be useful for stylized UI,
retro-inspired designs, or situations where you want clearly defined color zones.
@available(iOS 18.0, *)
struct InterpolationComparison: View {
var body: some View {
VStack(spacing: 0) {
// Smooth (Bezier) interpolation
MeshGradient(
width: 3, height: 3,
points: [
SIMD2(0.0, 0.0), SIMD2(0.5, 0.0), SIMD2(1.0, 0.0),
SIMD2(0.0, 0.5), SIMD2(0.5, 0.5), SIMD2(1.0, 0.5),
SIMD2(0.0, 1.0), SIMD2(0.5, 1.0), SIMD2(1.0, 1.0)
],
colors: [
.red, .yellow, .green,
.purple, .white, .cyan,
.blue, .mint, .orange
],
smoothsColors: true
)
// Linear interpolation
MeshGradient(
width: 3, height: 3,
points: [
SIMD2(0.0, 0.0), SIMD2(0.5, 0.0), SIMD2(1.0, 0.0),
SIMD2(0.0, 0.5), SIMD2(0.5, 0.5), SIMD2(1.0, 0.5),
SIMD2(0.0, 1.0), SIMD2(0.5, 1.0), SIMD2(1.0, 1.0)
],
colors: [
.red, .yellow, .green,
.purple, .white, .cyan,
.blue, .mint, .orange
],
smoothsColors: false
)
}
.ignoresSafeArea()
}
}
The difference is subtle at low grid resolutions but becomes pronounced as you increase the grid size or use
high-contrast color pairs. In most production scenarios, leave smoothsColors at its default true.
Apple Docs:
MeshGradient— SwiftUI
Animating Mesh Points
Static gradients are attractive. Animated mesh gradients are mesmerizing. Because MeshGradient conforms to View and
its points parameter accepts SIMD2<Float> values, you can drive point positions with SwiftUI state and animate them
with standard animation APIs.
Here is a mesh gradient that subtly shifts its interior points on a continuous loop — the kind of ambient background you might use on a movie detail screen while a trailer loads:
@available(iOS 18.0, *)
struct AnimatedMeshBackground: View {
@State private var isAnimating = false
var body: some View {
MeshGradient(
width: 3,
height: 3,
points: [
SIMD2(0.0, 0.0), SIMD2(0.5, 0.0), SIMD2(1.0, 0.0),
SIMD2(0.0, 0.5),
SIMD2(isAnimating ? 0.6 : 0.4,
isAnimating ? 0.4 : 0.6),
SIMD2(1.0, 0.5),
SIMD2(0.0, 1.0), SIMD2(0.5, 1.0), SIMD2(1.0, 1.0)
],
colors: [
.indigo, .purple, .indigo,
.orange, .white, .blue,
.red, .orange, .indigo
]
)
.onAppear {
withAnimation(
.easeInOut(duration: 4.0)
.repeatForever(autoreverses: true)
) {
isAnimating = true
}
}
.ignoresSafeArea()
}
}
Only the center point moves, oscillating between (0.6, 0.4) and (0.4, 0.6). The surrounding colors shift and distort
around it, creating an organic, breathing effect. The animation uses easeInOut with repeatForever so the motion
never feels abrupt.
Animating Multiple Points
For richer motion, animate several interior points independently using multiple state variables or a TimelineView for
frame-driven updates:
@available(iOS 18.0, *)
struct MultiPointAnimatedMesh: View {
@State private var phase: Bool = false
var body: some View {
MeshGradient(
width: 3,
height: 3,
points: [
SIMD2(0.0, 0.0),
SIMD2(phase ? 0.4 : 0.6, 0.0),
SIMD2(1.0, 0.0),
SIMD2(0.0, phase ? 0.6 : 0.4),
SIMD2(0.5, 0.5),
SIMD2(1.0, phase ? 0.4 : 0.6),
SIMD2(0.0, 1.0),
SIMD2(phase ? 0.6 : 0.4, 1.0),
SIMD2(1.0, 1.0)
],
colors: [
.red, .orange, .yellow,
.purple, .mint, .green,
.blue, .indigo, .cyan
]
)
.onAppear {
withAnimation(
.easeInOut(duration: 5.0)
.repeatForever(autoreverses: true)
) {
phase = true
}
}
.ignoresSafeArea()
}
}
Here the four edge midpoints oscillate in opposite directions, creating a gentle warping effect across the entire surface. The corner points stay fixed — this is important for avoiding visual tearing at the edges.
Warning: Avoid animating corner points away from the grid boundaries. When a corner moves inward, the area between the corner and the view edge fills with the
backgroundcolor, producing a visible seam.
Animating Colors
You can also animate the colors array. Because Color is Animatable through SwiftUI’s animation system, toggling
between two color palettes produces smooth cross-fades across the entire mesh:
@available(iOS 18.0, *)
struct ColorAnimatedMesh: View {
@State private var usePalette2 = false
private var colors: [Color] {
usePalette2
? [.blue, .indigo, .purple,
.cyan, .mint, .teal,
.green, .mint, .cyan]
: [.red, .orange, .yellow,
.pink, .white, .orange,
.purple, .red, .pink]
}
var body: some View {
MeshGradient(
width: 3, height: 3,
points: [
SIMD2(0.0, 0.0), SIMD2(0.5, 0.0), SIMD2(1.0, 0.0),
SIMD2(0.0, 0.5), SIMD2(0.5, 0.5), SIMD2(1.0, 0.5),
SIMD2(0.0, 1.0), SIMD2(0.5, 1.0), SIMD2(1.0, 1.0)
],
colors: colors
)
.onTapGesture {
withAnimation(.easeInOut(duration: 2.0)) {
usePalette2.toggle()
}
}
.ignoresSafeArea()
}
}
This pattern works well for theming — imagine tapping a Pixar movie poster and watching the background gradient morph from Woody’s warm desert palette to Buzz’s cool space palette.
Performance Considerations
MeshGradient renders on the GPU, so it is fast for its visual complexity. However, there are a few considerations for
production use.
Grid size matters linearly, not quadratically. Doubling the grid from 3x3 to 6x6 increases the number of control points from 9 to 36, but the rendering cost scales with the number of triangles the GPU rasterizes, not the number of control points. In practice, grids up to 6x6 render at 60fps on any device shipping iOS 18. Beyond 8x8, profile with Instruments before committing to the design.
Animation frame cost is low. Animating point positions or colors does not trigger a full view re-layout. SwiftUI
diffs the MeshGradient parameters and sends updated vertex data to the GPU. This makes continuous animations viable
even on older hardware like iPhone 15.
Compositing is where costs accumulate. A single MeshGradient is cheap. Stacking multiple mesh gradients with
.blendMode() modifiers, or overlaying them on complex view hierarchies with .blur() or .shadow(), can push the GPU
compositor past budget. If you notice frame drops, use the Core Animation instrument in Xcode to check the number of
offscreen passes.
Prefer drawingGroup() for complex overlays. If your mesh gradient sits behind a complex view tree with multiple
blend modes, wrapping the parent container in .drawingGroup() flattens the layer hierarchy into a single Metal
texture, reducing compositor overhead:
ZStack {
AnimatedMeshBackground()
MovieDetailContent()
}
.drawingGroup() // Flattens into a single GPU texture
Tip: On macOS,
MeshGradientrenders identically to iOS. However, window resizing triggers re-rasterization on every frame. If your mesh animates while the window resizes, consider pausing the animation during the resize event usingonReceivewithNSWindow.didResizeNotification.
When to Use (and When Not To)
| Scenario | Recommendation |
|---|---|
| Hero backgrounds | Excels here. Rich multi-directional blends with minimal code. |
| Loading states | Animated meshes make engaging placeholders. Use .mask(). |
| Branded color schemes | Map your brand palette to control points for ownable backgrounds. |
| Simple two-color gradients | Overkill. Use LinearGradient or RadialGradient instead. |
| Behind dense text | Use with caution. Apply .opacity() or overlay a material. |
| Below iOS 18 | Not available. Use if #available with a fallback gradient. |
When the gradient is purely decorative and not interactive, consider whether a pre-rendered image asset achieves the same visual at lower runtime cost. The answer is usually no — mesh gradients adapt to screen size, support dynamic animations, and respond to Dark Mode color changes without maintaining multiple asset variants.
Summary
MeshGradientdefines a 2D grid of control points, each with a position and color, producing rich multi-directional blends thatLinearGradientandRadialGradientcannot achieve.- Control points use unit coordinates from
(0, 0)to(1, 1). Keep corners pinned to the edges to avoid background bleed. smoothsColorstoggles between Bezier (organic) and linear (geometric) interpolation. Default totruefor most designs.- Animate point positions or colors using standard SwiftUI state and
withAnimationfor fluid, ambient motion effects. - Rendering is GPU-accelerated and efficient up to roughly 6x6 grids. Watch for compositor overhead when stacking multiple gradients or applying blur effects.
Ready to push your SwiftUI visuals further? Explore Visual Effects in SwiftUI to learn how to combine mesh gradients with scroll effects, shaders, and compositional transforms.