SF Symbols 7: Draw Animations, Gradients, and Magic Replace
You ship a polished feature, but the icons just sit there — static, lifeless, doing nothing to reinforce what the user just accomplished. iOS 26 changes that. SF Symbols 7 introduces draw animations that trace each symbol as if sketched by hand, gradient fills derived from a single color, a dedicated variable draw mode for progress visualization, and enhanced Magic Replace transitions. Together, these four capabilities turn your iconography from decoration into communication.
This post covers the four headline features of SF Symbols 7: the .drawOn / .drawOff symbol effects,
SymbolVariableValueMode.draw, SymbolColorRenderingMode.gradient, and Magic Replace enhancements. We won’t cover
custom symbol authoring or the SF Symbols app workflow — those deserve their own treatment.
Contents
- The Problem
- Draw Animations with .drawOn and .drawOff
- Variable Draw for Progress Visualization
- Gradient Rendering Mode
- Enhanced Magic Replace
- Performance Considerations
- When to Use (and When Not To)
- Summary
The Problem
Before iOS 26, symbol animations were limited to effects like .bounce, .pulse, .variableColor, and .scale. These
work well for drawing attention, but they don’t express creation or completion. Consider a movie rating feature
where you want to communicate that a score has been recorded:
struct MovieRatingView: View {
@State private var isRated = false
var body: some View {
VStack(spacing: 16) {
Image(systemName: "checkmark.seal.fill")
.font(.system(size: 48))
.foregroundStyle(.green)
.symbolEffect(.bounce, value: isRated)
Button("Rate Toy Story") {
isRated.toggle()
}
}
}
}
A bounce communicates “pay attention here,” not “this thing was just created.” There was no built-in effect that
conveyed a symbol being drawn into existence. You could reach for custom Path animations, but that meant abandoning SF
Symbols entirely and maintaining your own vector assets.
SF Symbols 7 solves this with purpose-built draw animations, richer rendering, and smoother transitions between related symbols.
Draw Animations with .drawOn and .drawOff
The .drawOn effect animates a symbol as if someone were tracing it with a pen — each layer appears following the
natural stroke order. The .drawOff effect does the reverse, erasing the symbol along its drawn path. Both use new draw
annotation data baked into the symbol definitions, so the choreography matches each symbol’s visual structure.
Basic Usage
Apply .drawOn the same way you apply any discrete symbol effect. Toggling isActive triggers the animation:
@available(iOS 26.0, *)
struct MovieCompletionBadge: View {
@State private var isComplete = false
var body: some View {
VStack(spacing: 20) {
Image(systemName: "checkmark.seal.fill")
.font(.system(size: 64))
.foregroundStyle(.accent)
.symbolEffect(.drawOn, isActive: isComplete)
Button(isComplete ? "Unmark" : "Mark as Watched") {
withAnimation {
isComplete.toggle()
}
}
}
}
}
When isComplete becomes true, the checkmark seal traces itself onto the screen. When it becomes false, the symbol
resets. This is a one-shot transition — the animation plays once per state change, not continuously.
Layer Control
Draw animations support three layer choreography options that control how a multi-layered symbol’s components are sequenced:
@available(iOS 26.0, *)
struct LayerDemoView: View {
@State private var animate = false
var body: some View {
HStack(spacing: 32) {
// Default: layers draw in sequence
Image(systemName: "film.stack")
.symbolEffect(.drawOn.byLayer, isActive: animate)
// All layers draw simultaneously
Image(systemName: "film.stack")
.symbolEffect(.drawOn.wholeSymbol, isActive: animate)
// Each layer draws independently with distinct timing
Image(systemName: "film.stack")
.symbolEffect(.drawOn.individually, isActive: animate)
}
.font(.system(size: 44))
}
}
.byLayer is the default and usually the best choice — it produces the calligraphic, hand-drawn feel that makes draw
animations distinctive. .wholeSymbol draws everything at once, which works well for simple, single-layer symbols.
.individually gives each layer its own independent timing, creating a staggered reveal that suits complex multi-part
symbols.
Draw Off for Removal
.drawOff reverses the animation, erasing the symbol along its stroke path. This pairs naturally with .drawOn for
toggle states:
@available(iOS 26.0, *)
struct FavoriteToggle: View {
@State private var isFavorite = false
var body: some View {
Button {
isFavorite.toggle()
} label: {
Image(systemName: "heart.fill")
.font(.system(size: 36))
.foregroundStyle(.pink)
.symbolEffect(
isFavorite ? .drawOn : .drawOff,
isActive: isFavorite
)
}
}
}
Using Draw as a Transition
You can also use .drawOn and .drawOff as transitions, which is useful when a symbol appears or disappears as part of
a conditional view:
@available(iOS 26.0, *)
struct DownloadCompleteView: View {
@State private var showSuccess = false
var body: some View {
VStack {
if showSuccess {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 56))
.foregroundStyle(.green)
.transition(.symbolEffect(.drawOn))
}
Button("Download Finding Nemo") {
showSuccess = true
}
}
}
}
Apple Docs:
DrawOnSymbolEffect— Symbols framework
Variable Draw for Progress Visualization
SF Symbols has supported variableValue since iOS 16, letting you control which layers of a symbol are filled based on
a 0.0...1.0 value. SF Symbols 7 adds a new variable value mode called .draw that changes how that value is
visualized. Instead of filling or dimming layers discretely, Variable Draw adjusts the drawn length of each variable
layer, turning each layer into a smooth progress indicator.
Applying Variable Draw
Use the symbolVariableValueMode(_:) modifier with .draw to activate this behavior:
@available(iOS 26.0, *)
struct RenderProgressView: View {
@State private var progress: Double = 0.0
var body: some View {
VStack(spacing: 24) {
Image(systemName: "waveform", variableValue: progress)
.font(.system(size: 56))
.foregroundStyle(.blue)
.symbolVariableValueMode(.draw)
Slider(value: $progress, in: 0...1)
.padding(.horizontal, 40)
Text("Rendering: \(Int(progress * 100))%")
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}
As progress increases from 0.0 to 1.0, each variable layer in the waveform symbol progressively draws itself. This
produces a fluid, analog feel that is far more expressive than the older discrete fill behavior.
Choosing Between Cumulative and Draw
The default variable value mode (.cumulative) fills layers in sequence — layer 1 fills completely before layer 2
begins. .draw instead adjusts the drawn path length of each individual layer simultaneously, which conveys continuous
progress rather than stepped thresholds. Use .cumulative for signal-strength indicators (like Wi-Fi bars) and .draw
for smooth, continuous progress feedback.
Note: Not every symbol supports Variable Draw. Symbols must have variable layer annotations and draw data. Check the SF Symbols app to confirm support before relying on
.drawmode in production.
Gradient Rendering Mode
SF Symbols 7 introduces a new color rendering mode that automatically generates a smooth linear gradient from a single
source color. This is separate from SymbolRenderingMode (which controls which layers get which colors).
SymbolColorRenderingMode controls how a given color fills each layer.
Flat vs. Gradient
Before iOS 26, every layer in a symbol was filled with a flat, solid color — the only option was what we now call
.flat. SF Symbols 7 adds .gradient, which derives a linear gradient from your tint color:
@available(iOS 26.0, *)
struct GradientComparisonView: View {
var body: some View {
HStack(spacing: 40) {
VStack {
Image(systemName: "popcorn.fill")
.font(.system(size: 56))
.foregroundStyle(.orange)
.symbolColorRenderingMode(.flat)
Text("Flat")
.font(.caption)
}
VStack {
Image(systemName: "popcorn.fill")
.font(.system(size: 56))
.foregroundStyle(.orange)
.symbolColorRenderingMode(.gradient)
Text("Gradient")
.font(.caption)
}
}
}
}
The gradient rendering adds a subtle sense of lighting and depth to symbols. It works across all four rendering modes —
monochrome, hierarchical, palette, and multicolor — and composes with your existing foregroundStyle and
symbolRenderingMode choices.
Combining with Rendering Modes
Gradient color rendering is orthogonal to symbol rendering modes. You can combine them freely:
@available(iOS 26.0, *)
struct CombinedRenderingView: View {
var body: some View {
HStack(spacing: 32) {
// Hierarchical + Gradient
Image(systemName: "theatermasks.fill")
.symbolRenderingMode(.hierarchical)
.symbolColorRenderingMode(.gradient)
.foregroundStyle(.purple)
// Palette + Gradient
Image(systemName: "theatermasks.fill")
.symbolRenderingMode(.palette)
.symbolColorRenderingMode(.gradient)
.foregroundStyle(.blue, .cyan)
// Multicolor + Gradient
Image(systemName: "theatermasks.fill")
.symbolRenderingMode(.multicolor)
.symbolColorRenderingMode(.gradient)
}
.font(.system(size: 48))
}
}
The gradient is derived automatically from the colors you provide. You cannot currently configure the gradient’s direction, stops, or type — Apple generates the gradient parameters for you based on the symbol’s geometry.
Apple Docs:
SymbolColorRenderingMode— SwiftUI
Enhanced Magic Replace
Magic Replace was introduced in SF Symbols 6 (iOS 18) as an intelligent transition between related symbols. Instead of a generic crossfade, Magic Replace animates shared structural elements — adding a slash, toggling a badge, filling or emptying — producing transitions that feel physically meaningful.
SF Symbols 7 deepens this by integrating draw animations into Magic Replace. When you transition between two symbols that both support draw data, the outgoing symbol draws off while the incoming symbol draws on, creating a fluid, continuous movement.
Using Magic Replace
Magic Replace is applied through .contentTransition(.symbolEffect(.replace)). The system automatically determines
whether a magic transition is possible between the source and destination symbols:
@available(iOS 26.0, *)
struct MovieSoundToggle: View {
@State private var isMuted = false
var body: some View {
Button {
withAnimation {
isMuted.toggle()
}
} label: {
Image(
systemName: isMuted
? "speaker.slash.fill"
: "speaker.wave.2.fill"
)
.contentTransition(.symbolEffect(.replace))
.font(.system(size: 44))
.foregroundStyle(.primary)
}
}
}
When isMuted toggles, the speaker waves animate away and the slash draws itself in — or vice versa. The system
handles the choreography. If the two symbols don’t share a magic-compatible relationship, the transition falls back to
the standard replace animation.
Explicit Fallback Control
You can specify a fallback strategy for cases where magic transition is unavailable:
@available(iOS 26.0, *)
struct PlaybackControlView: View {
@State private var isPlaying = false
var body: some View {
Button {
withAnimation {
isPlaying.toggle()
}
} label: {
Image(
systemName: isPlaying
? "pause.circle.fill"
: "play.circle.fill"
)
.contentTransition(
.symbolEffect(.replace.magic(fallback: .downUp))
)
.font(.system(size: 56))
}
}
}
The .magic(fallback:) modifier lets you choose .downUp, .upUp, or .offUp as the fallback transition when a magic
path doesn’t exist between the two symbols. This gives you predictable behavior even when the symbol pair doesn’t
support Magic Replace.
Tip: Magic Replace works best with symbol pairs from the same family — like
bell/bell.slash,wifi/wifi.slash, orspeaker.wave.2.fill/speaker.slash.fill. If you’re using unrelated symbols, the fallback transition will fire instead.Apple Docs:
ReplaceSymbolEffect— Symbols framework
Performance Considerations
Draw animations and gradient rendering add GPU work. In most contexts the cost is negligible, but there are scenarios where it adds up.
Symbol effects in lists. Applying .drawOn to every cell in a List or LazyVStack creates N simultaneous Core
Animation transactions. If you need draw animations in a scrolling context, trigger them selectively — for example,
only on the cell the user just tapped, not on all visible cells.
Gradient rendering at scale. .symbolColorRenderingMode(.gradient) renders each symbol layer with a gradient fill
instead of a flat color. For a handful of symbols in a toolbar or tab bar, the overhead is invisible. For a grid of 50+
symbols all using gradient rendering, profile with Instruments to confirm frame rates stay smooth.
Variable Draw with high-frequency updates. Binding variableValue to a continuously updating source (like an audio
level meter at 60 updates per second) is fine for a single symbol. If you drive multiple Variable Draw symbols
simultaneously, consider throttling updates to 15-30 Hz to avoid saturating the render pipeline.
Magic Replace composition. Each Magic Replace transition involves computing a structural diff between two symbol images at the Core Animation level. This is a one-shot cost per transition, not a per-frame cost, so it’s unlikely to cause issues unless you’re triggering dozens of replacements simultaneously.
Tip: Use the Core Animation Instruments template to profile symbol effect performance. Look at the “Commits” track to see how many CA transactions your symbol animations generate per frame.
When to Use (and When Not To)
| Scenario | Recommendation |
|---|---|
| Confirming a completed action | Use .drawOn — the hand-drawn reveal communicates “just happened” |
| Continuous progress | Use Variable Draw with .symbolVariableValueMode(.draw) |
| Toolbar and tab bar icons | Apply .symbolColorRenderingMode(.gradient) for subtle depth |
| Toggling related states | Use .contentTransition(.symbolEffect(.replace)) for Magic Replace |
| Attention-grabbing alerts | Stick with .bounce or .pulse — draw animations are too slow |
| Static decorative symbols | Skip effects entirely — no animation overhead needed |
| Large grids of symbols | Use .flat color rendering and avoid draw effects for performance |
Draw animations shine when they reinforce a moment in the user experience — a task completed, a state changed, a
value reached. They are not a replacement for .bounce or .pulse, which serve a different purpose (drawing attention
vs. expressing creation). Think of .drawOn as a curtain call and .bounce as a spotlight.
Summary
.drawOnand.drawOffanimate symbols along their stroke paths, bringing a hand-drawn feel to state transitions. Control layer choreography with.byLayer,.wholeSymbol, or.individually..symbolVariableValueMode(.draw)turns variable-value symbols into smooth, continuous progress indicators by adjusting each layer’s drawn path length..symbolColorRenderingMode(.gradient)generates a linear gradient from a single color, adding depth to symbols across all rendering modes with zero configuration.- Magic Replace now integrates draw animations, so transitioning between related symbols like
speaker.wave.2.fillandspeaker.slash.fillfeels physically continuous. - Profile symbol-heavy UIs with Instruments — draw effects and gradient rendering add GPU work that compounds in scrolling contexts.
These APIs pair naturally with the new Liquid Glass design language in iOS 26. Gradient-rendered symbols sitting on glass surfaces look cohesive out of the box, and draw animations add the kind of purposeful motion that Liquid Glass’s morphing transitions encourage. For a broader look at symbol animation techniques that predate iOS 26, see SwiftUI Animations.