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

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:

  1. Position — a SIMD2<Float> in a unit coordinate space where (0, 0) is the top-left corner and (1, 1) is the bottom-right.
  2. Color — a Color value 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
)
  • width and height define the grid dimensions. A 3x3 grid has 9 control points, a 4x4 grid has 16.
  • points is a flat array of SIMD2<Float> values, ordered row by row from top-left to bottom-right.
  • colors is a flat array of Color values matching the same order.
  • smoothsColors toggles 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 background color, 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, MeshGradient renders 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 using onReceive with NSWindow.didResizeNotification.

When to Use (and When Not To)

ScenarioRecommendation
Hero backgroundsExcels here. Rich multi-directional blends with minimal code.
Loading statesAnimated meshes make engaging placeholders. Use .mask().
Branded color schemesMap your brand palette to control points for ownable backgrounds.
Simple two-color gradientsOverkill. Use LinearGradient or RadialGradient instead.
Behind dense textUse with caution. Apply .opacity() or overlay a material.
Below iOS 18Not 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

  • MeshGradient defines a 2D grid of control points, each with a position and color, producing rich multi-directional blends that LinearGradient and RadialGradient cannot achieve.
  • Control points use unit coordinates from (0, 0) to (1, 1). Keep corners pinned to the edges to avoid background bleed.
  • smoothsColors toggles between Bezier (organic) and linear (geometric) interpolation. Default to true for most designs.
  • Animate point positions or colors using standard SwiftUI state and withAnimation for 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.