SwiftUI MeshGradient: The Complete Guide to Animated Multi-Color Gradients in iOS 26
Build animated multi-color mesh gradients in SwiftUI with iOS 26's Liquid Glass. Runnable code, color-space tips, and the gotchas I've hit in production.
SwiftUI MeshGradient is a shape style that blends an arbitrary grid of colored control points into a smooth, multi-color gradient, letting you produce the painterly, soft-glow backgrounds that powered the iOS 18 wallpapers and now show up everywhere in iOS 26's Liquid Glass UI. It was introduced in iOS 18 / macOS 15 / visionOS 2, runs on the GPU via Metal, and animates implicitly when you change its control points. This guide walks through the API, the gotchas I've hit shipping it in production, and the patterns I now reach for whenever a single linear gradient stops feeling like enough.
MeshGradient requires iOS 18, macOS 15, tvOS 18, watchOS 11, or visionOS 2 as a minimum deployment target. There is no back-port.
You define a width × height grid of SIMD2<Float> points and a flat array of Color values; the count must equal width * height.
Animate gradients by mutating the points or colors inside a withAnimation block, or drive continuous motion via TimelineView.
Use smoothsColors: true for soft transitions and colorSpace: .perceptual to avoid muddy mid-tones.
iOS 26 pairs MeshGradient with the new .glassEffect() material for hero backgrounds that react to scroll position and motion.
Mesh rendering is GPU-accelerated, but reading the same gradient in multiple views causes redundant Metal passes, so cache it in a stored property.
What is a mesh gradient in SwiftUI?
A mesh gradient is a 2D grid of colored control points where SwiftUI interpolates the colors across the surface using Bezier patches. Where a LinearGradient is a one-dimensional sweep between stops and a RadialGradient is a circular falloff from a center, a MeshGradient is a quilt: every cell in the grid blends into its neighbours, and you can warp the cell boundaries by moving the corner points off the regular grid. The math is the same Coons-patch interpolation that After Effects and Figma use for their mesh tools, so designers handing off mocks from those apps can map them onto SwiftUI almost 1:1.
The framework exposes it as a ShapeStyle, which means anywhere you can pass a Color or a LinearGradient (.fill(), .background(), .foregroundStyle(), the fill: parameter on shapes) you can pass a MeshGradient. The grid dimensions you pick determine how many control points you have to think about: a 2×2 mesh is four colors and behaves a lot like a four-point bilinear gradient; a 4×4 mesh gives you sixteen points and enough flexibility to mimic the iOS 18 dynamic wallpapers. Honestly, I'd start at 3×3 for most UI work. It's the sweet spot between expressiveness and the amount of state you have to wrangle.
How do you create a MeshGradient in SwiftUI?
You initialize MeshGradient with a width, a height, a flat array of SIMD2<Float> control points laid out row-major across the unit square (0.0 to 1.0 on each axis), and a parallel flat array of Color values. The two arrays must each have exactly width * height elements or the initializer traps at runtime. Here's the canonical three-by-three example I keep in a snippet:
A few details that aren't obvious from the docs. First, points on the outer boundary should usually stay pinned to the unit-square edges (x or y equal to 0.0 or 1.0). If you pull an edge point inward you'll get a visible cut-off where the gradient ends and the underlying view shows through. Second, the colors are interpreted in the gradient's color space; pass colorSpace: .perceptual if you're seeing muddy grey transitions between, say, pure red and pure blue. Third, set smoothsColors: true (it's the default in iOS 26, but was opt-in in iOS 18) to use a cubic interpolation that hides the patch seams. The cleaner version of the snippet looks like this:
The background color shows through any rendering gaps and during the brief frame before Metal compiles the first patch. Set it to something close to the average gradient color so the first paint doesn't flash. I've been bitten by leaving it as the default clear on app launch screens; the flash reads as a glitch even though it's gone in 16 milliseconds.
How do you animate a MeshGradient?
Mesh gradients animate the same way every other ShapeStyle does in SwiftUI: change the inputs inside a withAnimation block and the framework interpolates. The catch is that MeshGradient doesn't itself conform to Animatable. What's actually being animated is the @State array of points or colors driving the view body. That distinction matters when you write a custom transition or wrap the gradient in a child view: you need to pass the state down and recreate the gradient inside the child, not pass a prebuilt MeshGradient. Here's a continuous breathing animation I use for hero backgrounds:
Two things to internalize here. The TimelineView(.animation) rebuilds the body every frame the system is willing to give you (typically 60 or 120 Hz on ProMotion). That's wasteful if your animation completes in two seconds. For finite animations, prefer withAnimation(.smooth(duration: 2)) mutating a @State array and let SwiftUI's animation system run the interpolation. The other thing is that animating both the points and the colors at once is visually overwhelming. Pick one. I almost always animate points and keep colors fixed, because moving points crossing in screen space reads more like a "Liquid Glass" effect than crossfading.
For gesture-driven mesh distortion (a popular pattern for onboarding hero screens), use .gesture(DragGesture()) to map the drag translation to a single control point's offset, and wrap the assignment in withAnimation(.interactiveSpring()). The result feels exactly like the iOS 26 lock-screen wallpaper response.
MeshGradient vs LinearGradient: when to use each
There's still a place for the older gradient types. The decision usually comes down to how much color variation you need and how much GPU budget you're willing to spend.
Dimension
MeshGradient
LinearGradient
RadialGradient
Minimum OS
iOS 18 / macOS 15
iOS 13 / macOS 10.15
iOS 13 / macOS 10.15
Color stops
4–64 typical
2–8 typical
2–8 typical
Direction control
Free 2D grid
Single axis
Radial only
Rendering cost
Higher (Metal patches)
Very low
Low
Animation
Points + colors
Stops + colors
Center + colors
Best for
Hero, lock-screen, brand surfaces
Buttons, dividers, navigation bars
Spotlight effects, focus rings
Color space control
Yes
Yes (iOS 17+)
Yes (iOS 17+)
My rule of thumb: if the design needs more than one direction of color blending, reach for MeshGradient. If it's a button background, a navigation header, or anything where you'd be embarrassed to drop the iOS 17 deployment target, stick with LinearGradient. The cost difference isn't dramatic on modern A-series silicon (a 4×4 mesh costs roughly a single 1080p Metal pass per frame), but it adds up if you stack many of them in a scroll feed.
MeshGradient in iOS 26: Liquid Glass and scroll-reactive backgrounds
iOS 26 didn't add new public methods on MeshGradient itself, but it changed two things around it that matter. First, smoothsColors now defaults to true on a fresh project. Second, the new .glassEffect() modifier composes cleanly on top of a mesh background, picking up its colors and refracting them through the Liquid Glass material. The combination is what powers a lot of the new Apple Music and App Store hero sections. The pattern I'm shipping looks like this:
Pair this with scrollTransition on a parent ScrollView and the glass card warps over the mesh as the user scrolls. The SwiftUI ScrollView in iOS 26 guide walks through the scroll APIs in detail; the mesh fits in wherever you'd put a Color or LinearGradient behind a scroll target.
Mesh gradients render on the GPU, but they aren't free. A 4×4 perceptual mesh at 1290×2796 (iPhone 17 Pro Max) is roughly the same cost as a small SceneKit pass: fine for one full-screen instance, painful if you have ten of them in a card grid. Three things have caught me in production.
Animating colors triggers shader recompiles
When you change the structure of the gradient (the count of points or colors, the color space, smoothsColors), Metal has to recompile a new pipeline state. Animating just the point positions or just the colors is fine. Switching between, say, a 3×3 and a 4×4 mesh mid-animation is not; you'll see a one-frame stutter. Decide on a grid size up front and stick with it.
Implicit animations on the wrong state
If you wrap the MeshGradient initializer's arguments in a method call that recomputes on every body invocation, SwiftUI loses the identity it needs to animate between two states. Make the points and colors explicit @State properties, or compute them in a memoized helper. The breathing example above works because animatedPoints(phase:) is a pure function of phase (same input, same output), and phase itself is what's animating.
Color space chooses your mids
The default color space interpolates in sRGB, which mixes red and blue into a muddy grey instead of magenta. Pass colorSpace: .perceptual if any pair of adjacent corner colors is more than ~120° apart on the color wheel. The OKLab math underneath does what your eye expects.
Tooling: designing mesh gradients without guessing
The hardest part of working with MeshGradient isn't the API, it's picking colors and point positions that look intentional. Hand-coding sixteen SIMD2<Float> values is a recipe for "computer-generated" backgrounds. A few tools have made this dramatically easier.
Mesh Gradient Editor (Mac App Store): visual editor that exports a SwiftUI snippet you can paste straight into your project. The export includes the points, colors, and a sensible colorSpace setting.
SwiftUI previews with sliders: wire each control point to a @State tuple and bind to Slider views in a debug overlay. I keep a #if DEBUG mesh editor view in every project that uses meshes heavily.
Figma mesh plugins: designers can build the mesh in Figma and export the points; map their 0–100 percentages to 0.0–1.0 floats and you're done.
For deeper animation choreography around the mesh (springs, keyframes, custom transitions), the SwiftUI animations guide covers the timing curves that pair best with mesh point morphs. The default .smooth spring is what Apple uses on its own mesh-driven backgrounds, and it's a sensible default until you have a reason to pick something else. The original WWDC 2024 "What's new in SwiftUI" session introduces MeshGradient with a live demo that's still worth twenty minutes of your time.
Frequently Asked Questions
What iOS version supports MeshGradient?
MeshGradient requires iOS 18, iPadOS 18, macOS 15 (Sequoia), tvOS 18, watchOS 11, or visionOS 2 as a minimum deployment target. There is no official back-port to earlier OS versions, so you'll need to fall back to crossed LinearGradients with .blendMode(.screen) if you support iOS 17 or earlier.
How many colors can a MeshGradient have?
The colors array length must equal width * height, so a 4×4 mesh has 16 colors and an 8×8 mesh has 64. There is no hard upper bound documented, but I'd treat 64 as the practical ceiling. Metal pipeline state objects scale with grid size and you start to see compile cost on first paint above that.
Can you use MeshGradient on macOS and visionOS?
Yes. It ships in the same SwiftUI module across all four platforms (macOS 15, visionOS 2, tvOS 18, and watchOS 11) with identical APIs. On visionOS it composes with the platform's material effects, and on macOS it respects the window's colorSpace for wide-gamut displays automatically.
Why does my MeshGradient look pixelated or banded?
You're almost certainly using sRGB interpolation between high-saturation colors. Add colorSpace: .perceptual to the initializer and the banding will disappear. If you still see it, you're probably rendering into a low-precision drawing context. Check that you aren't snapshotting the view to an 8-bit UIImage for caching.
Does MeshGradient support dark mode?
It supports any Color you pass it, including dynamic asset-catalog colors that respond to colorScheme. The catch is that color interpolation happens after each dynamic color resolves, so a gradient that looks balanced in light mode can produce unexpected mids in dark mode. I usually design two separate mesh palettes and switch via @Environment(\.colorScheme).
How to use SwiftUI PhotosPicker in iOS 26 for multi-select, video filtering, and file-based loading. Covers race conditions, iCloud progress, and VoiceOver.
Swift RegexBuilder gives you type-safe regular expressions with named captures, inline transforms, and Unicode-correct matching by default. Learn the DSL, compare it to NSRegularExpression, and avoid production traps.
Build SwiftUI Metal shaders in iOS 26 with colorEffect, distortionEffect, and layerEffect. Includes MSL setup, TimelineView animation, and GPU cost tips with working code.