Animating SF Symbols in SwiftUI
SF Symbols are a natural choice for icons in SwiftUI apps. The system provides a very large symbol catalog, and extensive customization options. Size, weight, rendering mode, and color can all be adjusted to match the surrounding UI, making symbols easy to integrate across an app.
Beyond static icons, SF Symbols also support animation. These animations are called symbol effects. They allow icons to respond to state changes and interactions, bringing more life to the interface without custom drawing or complex animation code.
Symbol effects are provided by the Symbols framework, which is implicitly available when working with SwiftUI. There is a built-in collection of effects that can be applied to any symbol, including custom symbols. Effects are configurable, and options can be chained to create very specific behavior. The SF Symbols app documents each effect and its supported options.
In this post, we'll explore the SwiftUI APIs for animating SF Symbols and use the groupings based on protocol conformance in the Symbols framework as a mental model. We'll look at four effect groups: indefinite effects (IndefiniteSymbolEffect), discrete effects (DiscreteSymbolEffect), content transitions (ContentTransitionSymbolEffect), and transitions (TransitionSymbolEffect). Each group represents a distinct animation intent and naturally aligns with the SwiftUI API patterns used to apply these animations.
# Indefinite symbol effects
Indefinite symbol effects apply a modification that remains in place for as long as the effect is active. This category includes the largest set of effects, such as ScaleSymbolEffect, RotateSymbolEffect, BreatheSymbolEffect, and many others. The full list of animations that conform to the IndefiniteSymbolEffect protocol can be found in the Apple documentation.
Indefinite effects can be configured using the symbolEffect(_:options:isActive:) modifier, where the isActive parameter is used to turn the effect on or off.
Such effects can help provide feedback for a continuous user interaction, like an active hover, for example. Here is how we can scale symbols up when the mouse is over them.
struct HoverScalingSymbol: View {
let systemName: String
@State private var isHovering = false
var body: some View {
Image(systemName: systemName)
.symbolEffect(.scale.up, isActive: isHovering)
.onHover { hovering in
isHovering = hovering
}
}
}
Some animations in the indefinite effects group, such as breathe, bounce, pulse, and rotate, repeat forever while active. These are great for indicating an ongoing activity or process.
Image(systemName: "record")
.symbolVariant(.circle)
.symbolEffect(.breathe, isActive: isRecording)
If we omit the isActive parameter for such effects, it will default to true and the animation will be active forever.
Indefinite effects really shine with configuration options that control how the animation progresses and repeats over time. By chaining these options, we can arrive at a variant that best suits our use case. Here is an example of how the variable color animation can be customized:
Image(systemName: "waveform")
.symbolEffect(.variableColor.iterative.reversing)
# Discrete symbol effects
Discrete symbol effects perform a transient, one-off animation that is triggered by a change in a specific value. These effects are great for drawing attention to an element in the UI or indicating that an action has taken place.
A few animation types that support indefinite behavior also support discrete behavior. The full list can be found in the conforming types of the DiscreteSymbolEffect protocol.
To add a discrete animation to a symbol in a SwiftUI app, we can use the symbolEffect(_:options:value:) modifier. Here is how we can trigger a one-off bounce effect every time a value is incremented:
struct BasketView: View {
@State private var numOfItems = 0
var body: some View {
VStack(spacing: 30) {
Image(systemName: "basket")
.symbolEffect(.bounce, value: numOfItems)
Button("Add to basket") {
numOfItems += 1
}
}
}
}
To customize how the effect is performed, how fast and how many times, we can also provide the options parameter. For example, we might want to bounce the symbol twice very quickly.
Image(systemName: "basket")
.symbolEffect(
.bounce,
options: .repeat(2).speed(2),
value: numOfItems
)
# Symbol effect content transitions
When we want to switch between symbol variants or distinct symbols, we can take advantage of built-in symbol animations with the symbol effect content transitions.
When a symbolEffect content transition is applied without any configuration options, the system will choose the most appropriate transition for the context.
In the example below, the slash will be drawn on and off as we toggle the setting.
struct NotificationButton: View {
@State private var notificationsEnabled = true
var body: some View {
Button {
notificationsEnabled.toggle()
} label: {
Image(systemName: "bell")
}
.buttonStyle(.borderless)
.symbolVariant(notificationsEnabled ? .none : .slash)
.contentTransition(.symbolEffect)
}
}
When switching between unrelated symbols the system will do its best to interpolate between the two.
struct WeatherSymbol: View {
@State private var isSunny = true
var body: some View {
Image(systemName: isSunny ? "sun.max" : "cloud")
.contentTransition(.symbolEffect)
.onTapGesture {
isSunny.toggle()
}
}
}
We can still customize the transition with parameters passed to the symbolEffect(_:options:) function, for example .contentTransition(.symbolEffect(.replace.upUp)), but I've found that the defaults work best for many cases.
# Symbol effect transitions
When adding a symbol to the hierarchy, or removing it, we can provide a symbol effect transition using the transition(_:) modifier.
if isSnowing {
Image(systemName: "snowflake")
.transition(.symbolEffect)
}
Note, that unlike other types of transitions, the symbolEffect transition will be activated even if we don't apply an implicit animation to the view hierarchy with the animation(_:value:) modifier or wrap the state modification into a withAnimation {} function.
Instead of relying on the default behavior of the symbol effect transition, we can provide an explicit effect type to achieve a more interesting animation. For example, on iOS and macOS 26, we can set the drawOn as a preference and it will be activated for symbols that support this new animation type.
if isWindy {
Image(systemName: "wind")
.transition(.symbolEffect(.drawOn))
}
SF Symbols are very flexible, and they continue to be updated every year with new behaviors and customization options. Symbol effect animations were introduced in iOS 17, and have significantly evolved since then. It's great to see how easy it is to use these animations in our SwiftUI apps and adapt them to different interaction patterns and use cases.
If you are looking to build a strong foundation in SwiftUI, my book SwiftUI Fundamentals is a great place to start. It takes a deep dive into the framework's core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.
For more resources on Swift and SwiftUI, check out my other books and book bundles.



