home / skills / charleswiltgen / axiom / axiom-sf-symbols
This skill helps you implement and troubleshoot SF Symbols rendering modes, effects, and Draw animations across iOS 17 and SF Symbols 7
npx playbooks add skill charleswiltgen/axiom --skill axiom-sf-symbolsReview the files below or copy the command above to add this skill to your agents.
---
name: axiom-sf-symbols
description: Use when implementing SF Symbols rendering modes, symbol effects, animations, custom symbols, or troubleshooting symbol appearance - covers the full symbol effects system from iOS 17 through SF Symbols 7 Draw animations in iOS 26
license: MIT
compatibility: iOS 17+, iOS 18+ (Wiggle/Rotate/Breathe), iOS 26+ (Draw animations)
metadata:
version: "1.0.0"
---
# SF Symbols — Effects, Rendering, and Custom Symbols
## When to Use This Skill
Use when:
- Choosing between rendering modes (Monochrome, Hierarchical, Palette, Multicolor)
- Implementing symbol effects or animations (Bounce, Pulse, Scale, Wiggle, Rotate, Breathe, Draw)
- Working with SF Symbols 7 Draw On/Off animations
- Creating custom symbols in the SF Symbols app
- Troubleshooting symbol colors, effects not playing, or weight mismatches
- Deciding which effect matches a specific UX purpose
- Handling accessibility with symbol animations (Reduce Motion)
#### Related Skills
- Use `axiom-sf-symbols-ref` for complete API reference with all modifiers, UIKit equivalents, and platform availability matrix
- Use `axiom-swiftui-animation-ref` for general SwiftUI animation (not symbol-specific)
- Use `axiom-hig-ref` for broader icon design guidelines
## Example Prompts
#### 1. "My SF Symbol shows as a single flat color but I want it to have depth with multiple shades. How do I fix this?"
> The skill covers rendering mode selection — Hierarchical for depth from a single color, Palette for explicit per-layer colors
#### 2. "I want my download button to animate when tapped, then show a spinning indicator while downloading, and animate to a checkmark when done."
> The skill covers effect selection: Bounce for tap feedback, Breathe/Pulse for in-progress, Replace with content transition for completion
#### 3. "I'm trying to use the new Draw animations from SF Symbols 7 but the effect isn't playing."
> The skill covers Draw On/Off implementation, playback modes, iOS 26 requirements, and common troubleshooting
#### 4. "How do I create a custom symbol that supports all rendering modes and the new Draw animation?"
> The skill covers custom symbol authoring workflow, template layers, Draw annotation with guide points
---
## Part 1: Rendering Mode Decision Tree
SF Symbols support 4 rendering modes. The right choice depends on your design intent.
### Quick Decision
```
Need depth from ONE color? → Hierarchical
Need specific colors per layer? → Palette
Want Apple's curated colors? → Multicolor
Just need a tinted icon? → Monochrome (default)
```
### Monochrome
The default mode. Every layer renders in the same color (your `foregroundStyle`).
```swift
Image(systemName: "cloud.rain.fill")
.foregroundStyle(.blue)
// All layers are blue
```
**When to use**: Simple tinted icons, matching text color, toolbar items, tab bar items.
### Hierarchical
Renders layers at different opacities derived from a **single** color. Primary layers are fully opaque; secondary and tertiary layers get progressively more transparent. Creates depth without specifying multiple colors.
```swift
Image(systemName: "cloud.rain.fill")
.symbolRenderingMode(.hierarchical)
.foregroundStyle(.blue)
// Cloud is full blue, rain drops are lighter blue
```
**When to use**: When you want visual depth but still want the icon to feel cohesive with a single hue. Most common choice for polished UI.
### Palette
Each layer gets an **explicit** color. Unlike Hierarchical, no automatic opacity derivation — you control each layer's color directly.
```swift
Image(systemName: "cloud.rain.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(.blue, .cyan)
// Cloud is blue, rain drops are cyan
```
**When to use**: Branded icons, status indicators where specific colors carry meaning, designs requiring exact color control.
**Gotcha**: If you provide fewer colors than layers, extra layers reuse the last color. If the symbol has 3 layers and you provide 2 colors, the third layer uses the second color.
### Multicolor
Uses Apple's predefined color scheme for each symbol. Colors are fixed — you cannot customize them.
```swift
Image(systemName: "cloud.rain.fill")
.symbolRenderingMode(.multicolor)
// Cloud is white, rain drops are blue (Apple's design)
```
**When to use**: Weather indicators, file type icons, or anywhere Apple's curated design intent matches your needs. Not all symbols support Multicolor — unsupported symbols fall back to Monochrome.
### Common Mistakes
| Mistake | Impact | Fix |
|---------|--------|-----|
| Using `.foregroundColor()` with Multicolor | Overrides Apple's colors | Remove foreground color modifier |
| Setting Palette with only 1 color | Looks like Monochrome | Provide colors for each layer |
| Assuming all symbols support Multicolor | Fallback to Monochrome | Check in SF Symbols app first |
| Using Hierarchical when layers need distinct meanings | Colors don't carry semantic intent | Use Palette instead |
---
## Part 2: Symbol Effects System
Symbol effects bring SF Symbols to life with motion. Every effect falls into one of four behavioral categories.
### Effect Categories
| Category | Trigger | Duration | Use Case |
|----------|---------|----------|----------|
| **Discrete** | Value change | One-shot | Tap feedback, event notification |
| **Indefinite** | `isActive` bool | Continuous until stopped | Loading states, ongoing processes |
| **Transition** | View insert/remove | One-shot | Appear/disappear with style |
| **Content Transition** | Symbol swap | One-shot | Replacing one symbol with another |
### Which Effect for Which UX Purpose
```
User tapped something → Bounce (discrete)
Something changed, draw attention → Wiggle (discrete, iOS 18+)
Ongoing process/loading → Pulse, Breathe, or Variable Color (indefinite)
Rotation indicates progress → Rotate (indefinite, iOS 18+)
Show/hide symbol → Appear/Disappear (transition)
Swap between two symbols → Replace (content transition)
Symbol enters with hand-drawn style → Draw On (iOS 26+)
Symbol exits with hand-drawn style → Draw Off (iOS 26+)
Progress indicator along path → Variable Draw (iOS 26+)
Scale up/down for emphasis → Scale (indefinite)
```
### Discrete Effects
Fire once when a value changes. The symbol performs the animation and returns to its resting state.
#### Bounce
The most common discrete effect. A brief, springy animation.
```swift
@State private var downloadCount = 0
Image(systemName: "arrow.down.circle")
.symbolEffect(.bounce, value: downloadCount)
```
The animation triggers each time `downloadCount` changes.
**Directional options**: `.bounce.up`, `.bounce.down`
#### Wiggle (iOS 18+)
A horizontal shake that draws attention to the symbol.
```swift
Image(systemName: "bell.fill")
.symbolEffect(.wiggle, value: notificationCount)
```
**Directional options**: `.wiggle.left`, `.wiggle.right`, `.wiggle.forward`, `.wiggle.backward`
`.forward` and `.backward` respect reading direction — use these for RTL support.
#### Rotate (as Discrete, iOS 18+)
A single rotation when triggered by value change.
```swift
Image(systemName: "arrow.trianglehead.2.clockwise")
.symbolEffect(.rotate, value: refreshCount)
```
**Options**: `.rotate.clockwise`, `.rotate.counterClockwise`
**By Layer**: Some symbols rotate only specific layers (e.g., fan blades spin but the housing stays fixed). Use `.rotate.byLayer` to activate this.
### Indefinite Effects
Run continuously while `isActive` is `true`. Stop when `isActive` becomes `false`.
#### Pulse
A subtle opacity pulse. Good for "waiting" states.
```swift
Image(systemName: "network")
.symbolEffect(.pulse, isActive: isConnecting)
```
#### Variable Color
Iterates through the symbol's layers, highlighting each in sequence. Creates a "filling up" or "cycling" look.
```swift
Image(systemName: "wifi")
.symbolEffect(.variableColor.iterative, isActive: isSearching)
```
**Variants**:
- `.variableColor.iterative` — highlights one layer at a time
- `.variableColor.cumulative` — progressively fills layers
- `.variableColor.reversing` — cycles back and forth
- Combine: `.variableColor.iterative.reversing`
#### Scale
Scales the symbol up or down.
```swift
Image(systemName: "mic.fill")
.symbolEffect(.scale.up, isActive: isRecording)
```
#### Breathe (iOS 18+)
A smooth, rhythmic scale animation — like the symbol is breathing.
```swift
Image(systemName: "heart.fill")
.symbolEffect(.breathe, isActive: isMonitoring)
```
**Variants**: `.breathe.plain` (scale only), `.breathe.pulse` (scale + opacity)
#### Rotate (as Indefinite, iOS 18+)
Continuous rotation for processing indicators.
```swift
Image(systemName: "gear")
.symbolEffect(.rotate, isActive: isProcessing)
```
### Effect Options
All effects accept `SymbolEffectOptions` via the `options` parameter.
```swift
// Repeat 3 times
.symbolEffect(.bounce, options: .repeat(3), value: count)
// Double speed
.symbolEffect(.pulse, options: .speed(2.0), isActive: true)
// Repeat continuously
.symbolEffect(.variableColor, options: .repeat(.continuous), isActive: true)
// Non-repeating (run once)
.symbolEffect(.breathe, options: .nonRepeating, isActive: true)
// Combine options
.symbolEffect(.bounce, options: .repeat(5).speed(1.5), value: count)
```
### Transition Effects
Used when a symbol-based view appears or disappears from the view hierarchy.
```swift
if showSymbol {
Image(systemName: "checkmark.circle.fill")
.transition(.symbolEffect(.appear))
}
```
**Available transitions**: `.appear`, `.disappear`
**Variants**: `.appear.up`, `.appear.down`, `.disappear.up`, `.disappear.down`
### Content Transitions
Used to animate from one symbol to another. Applied to the container, not the symbol.
```swift
@State private var isFavorite = false
Button {
isFavorite.toggle()
} label: {
Image(systemName: isFavorite ? "star.fill" : "star")
.contentTransition(.symbolEffect(.replace))
}
```
**Replace variants**:
- `.replace.downUp` — old symbol moves down, new moves up
- `.replace.upUp` — both move up
- `.replace.offUp` — old fades off, new moves up
#### Magic Replace
When two symbols share a common structure (like `star` and `star.fill`, or `pause.fill` and `play.fill`), Replace automatically performs a **Magic Replace** — morphing shared elements while transitioning differing parts. Magic Replace is the default behavior for `.replace` in iOS 18+. For explicit control:
```swift
// Explicit Magic Replace with fallback
.contentTransition(.symbolEffect(.replace.magic(fallback: .replace.downUp)))
```
---
## Part 3: SF Symbols 7 — Draw Animations (iOS 26+)
Draw animations simulate the natural flow of drawing a symbol with a pen. This is the signature new feature in SF Symbols 7.
### Draw On and Draw Off
**Draw On** animates a symbol appearing by "drawing" it stroke by stroke.
**Draw Off** animates a symbol disappearing by "erasing" it.
```swift
// Draw On — symbol draws in when isComplete becomes true
Image(systemName: "checkmark.circle")
.symbolEffect(.drawOn, isActive: isComplete)
// Draw Off — symbol draws out when isHidden becomes true
Image(systemName: "star.fill")
.symbolEffect(.drawOff, isActive: isHidden)
```
### Playback Modes
Control how multi-layer symbols animate their draw:
```swift
// By Layer (default) — staggered timing, layers overlap
Image(systemName: "square.and.arrow.up")
.symbolEffect(.drawOn.byLayer, isActive: showIcon)
// Whole Symbol — all layers draw simultaneously
Image(systemName: "square.and.arrow.up")
.symbolEffect(.drawOn.wholeSymbol, isActive: showIcon)
// Individually — sequential, each layer completes before next starts
Image(systemName: "square.and.arrow.up")
.symbolEffect(.drawOn.individually, isActive: showIcon)
```
**When to use each mode**:
- **By Layer** (default): Most natural feel, good for most symbols
- **Whole Symbol**: When the symbol should appear as one unit, not in parts
- **Individually**: When you want to emphasize each layer separately (storytelling, onboarding)
### Draw Off Direction
Draw Off supports controlling whether the animation plays forward or in reverse:
```swift
// Forward (default) — follows the draw path
.symbolEffect(.drawOff.nonReversed, isActive: isHidden)
// Reversed — erases in reverse order of how it was drawn
.symbolEffect(.drawOff.reversed, isActive: isErasing)
```
### Variable Draw
Variable Draw uses `SymbolVariableValueMode.draw` to partially draw a symbol's stroke path based on a 0.0 to 1.0 value — perfect for progress indicators.
```swift
Image(systemName: "thermometer.high", variableValue: temperature)
.symbolVariableValueMode(.draw) // iOS 26+
```
Compare with traditional Variable Color (which sets opacity per layer):
```swift
Image(systemName: "wifi", variableValue: signalStrength)
.symbolVariableValueMode(.color) // iOS 17+ (default behavior)
```
**Constraint**: A symbol can support both Variable Color and Variable Draw, but only one mode can be active at render time. Setting an unsupported mode has no visible effect.
### Gradient Rendering
SF Symbols 7 introduces `SymbolColorRenderingMode` for gradient fills generated from a single source color.
```swift
Image(systemName: "star.fill")
.symbolColorRenderingMode(.gradient) // iOS 26+
.foregroundStyle(.red)
```
| Mode | Description |
|------|-------------|
| `.flat` | Solid color fill (default) |
| `.gradient` | Axial gradient from source color |
Gradients work with all rendering modes and are most effective at larger sizes.
### Magic Replace with Draw
When using `.contentTransition(.symbolEffect(.replace))` between certain symbol pairs, the system now combines Draw Off on the outgoing symbol with Draw On for the incoming symbol. The enclosure (if shared, like a circle outline) is preserved while inner elements transition with draw animations.
```swift
// Automatic Draw-enhanced Magic Replace
Image(systemName: isComplete ? "checkmark.circle.fill" : "circle")
.contentTransition(.symbolEffect(.replace))
```
### Custom Symbol Draw Annotation
To enable Draw animations on custom symbols, annotate paths in the SF Symbols app:
1. **Open** your custom symbol in SF Symbols 7
2. **Select** a path layer
3. **Add guide points** to define draw direction:
- **Start point** (open circle): Where drawing begins
- **End point** (closed circle): Where drawing ends
- **Corner point** (diamond): Sharp direction changes
- **Bidirectional point**: Enables center-outward drawing
- **Attachment point**: Connects non-drawing decorative elements
4. **Minimum**: Two guide points per path (start and end)
5. **Test** using the Preview panel in SF Symbols app
**Option-drag** guide points for precise placement. Use context menus to configure direction and end caps.
---
## Part 4: Anti-Patterns
### Wrong Rendering Mode
| Pattern | Problem | Fix |
|---------|---------|-----|
| Palette with 1 color | Equivalent to Monochrome, wasted API call | Use Monochrome or provide multiple colors |
| Multicolor for branded icons | Can't customize Apple's fixed colors | Use Palette with brand colors |
| Hardcoded `.foregroundColor(.blue)` | Ignores Dark Mode, Dynamic Type, accessibility | Use `.foregroundStyle()` with semantic colors |
| Hierarchical for status indicators | Layers don't carry distinct meaning | Use Palette with semantic colors |
### Wrong Effect Choice
| Pattern | Problem | Fix |
|---------|---------|-----|
| Bounce for loading state | One-shot, doesn't convey "ongoing" | Use Pulse, Breathe, or Variable Color |
| Pulse for tap feedback | Too subtle for confirming action | Use Bounce |
| Continuous Rotate for non-mechanical symbols | Looks unnatural for organic shapes | Use Breathe for organic symbols |
| Draw On for transient state changes | Too dramatic for frequent toggles | Use Replace or Scale |
### Missing iOS Version Checks
```swift
// ❌ Crashes on iOS 17
Image(systemName: "bell")
.symbolEffect(.wiggle, value: count) // Wiggle requires iOS 18+
// ✅ Safe version check
Image(systemName: "bell")
.modifier(BellEffectModifier(count: count))
struct BellEffectModifier: ViewModifier {
let count: Int
func body(content: Content) -> some View {
if #available(iOS 18, *) {
content.symbolEffect(.wiggle, value: count)
} else {
content.symbolEffect(.bounce, value: count)
}
}
}
```
### Ignoring Reduce Motion
Symbol effects **automatically** respect the Reduce Motion accessibility setting — most effects are suppressed or simplified. However, if you're using effects to convey essential information (not just decoration), provide an alternative:
```swift
// Variable Color conveys WiFi strength — provide text fallback
Image(systemName: "wifi")
.symbolEffect(.variableColor, isActive: isSearching)
.accessibilityLabel("Searching for WiFi networks")
```
**Do not** disable Reduce Motion or try to force-play effects. The system handles this correctly.
### Missing Accessibility Labels
```swift
// ❌ VoiceOver says "star.fill"
Image(systemName: "star.fill")
// ✅ VoiceOver says "Favorite"
Image(systemName: "star.fill")
.accessibilityLabel("Favorite")
```
When using `.contentTransition(.symbolEffect(.replace))` to swap symbols, update the accessibility label to match the current state:
```swift
Image(systemName: isFavorite ? "star.fill" : "star")
.contentTransition(.symbolEffect(.replace))
.accessibilityLabel(isFavorite ? "Remove from favorites" : "Add to favorites")
```
---
## Part 5: Troubleshooting
### Effect Not Playing
**Symptom**: `.symbolEffect()` modifier applied but no animation visible.
1. **Check iOS version** — Bounce/Pulse/Scale require iOS 17+, Wiggle/Rotate/Breathe require iOS 18+, Draw requires iOS 26+
2. **Check Reduce Motion** — Settings > Accessibility > Motion > Reduce Motion. If on, most effects are suppressed
3. **Check trigger type** — Discrete effects need `value:` that changes. Indefinite effects need `isActive: true`. Transition effects need the view to actually enter/leave the hierarchy
4. **Check symbol compatibility** — Not all symbols support all effects. Open the SF Symbols app, select the symbol, and check the Animation inspector
5. **Check for conflicting effects** — Multiple `.symbolEffect()` modifiers on the same view can conflict. Use a single effect or combine with options
### Wrong Colors in Rendering Mode
**Symptom**: Symbol colors don't match expected appearance.
1. **Check rendering mode** — If you set `.foregroundStyle` but see only one color, you may need `.symbolRenderingMode(.palette)` or `.hierarchical`
2. **Check `.tint` vs `.foregroundStyle`** — In UIKit, `tintColor` affects Monochrome and Hierarchical. For Palette, use `UIImage.SymbolConfiguration(paletteColors:)`
3. **Check Multicolor support** — Not all symbols have Multicolor variants. Unsupported symbols fall back to Monochrome
4. **Check environment** — `.foregroundStyle` from a parent view may override your rendering mode. Apply `.symbolRenderingMode()` directly on the Image
### Custom Symbol Weight Mismatch
**Symptom**: Custom symbol looks too thin or too thick next to text or other symbols.
1. **Check template weight** — Custom symbols need weight variants matching the 9 SF Pro weights. Export from SF Symbols app handles this
2. **Check `.font()` alignment** — The symbol's weight follows the applied font weight. If using `.font(.title)`, ensure your custom symbol has appropriate weight variants
3. **Check scale** — `.imageScale(.small/.medium/.large)` affects overall size. Use `.font()` for weight matching
### Draw Animation Not Working on Custom Symbol
**Symptom**: `.symbolEffect(.drawOn)` applied to custom symbol but no draw animation occurs.
1. **Check guide points** — Custom symbols need Draw annotation with at least 2 guide points per path (start + end)
2. **Check SF Symbols app version** — Draw annotation requires SF Symbols 7+
3. **Check path structure** — Guide points must be placed on stroked paths, not fills. Convert fills to strokes where draw animation is desired
4. **Check layer structure** — Each annotatable layer needs its own guide points
---
## Part 6: Pressure Scenarios
### Scenario 1: "Just use a static image, symbols are overkill"
**Setup**: Designer provides PNG icons. Developer considers using them instead of SF Symbols.
**Why this matters**: Static PNGs don't adapt to Dynamic Type, Bold Text, Dark Mode, or accessibility settings. They also don't support symbol effects.
**Professional response**: "SF Symbols scale with text, support 9 weights, adapt to Dark Mode and Bold Text automatically, and enable animations without custom code. A PNG requires @1x/@2x/@3x variants, manual Dark Mode handling, manual Dynamic Type scaling, and custom animation code. The 10 minutes to find the right SF Symbol saves hours of asset management."
**Time cost of skipping**: 2-4 hours managing assets + ongoing maintenance vs 10 minutes finding the right symbol.
### Scenario 2: "We'll add animations later"
**Setup**: Sprint deadline. PM says animations are polish and can wait.
**Why this matters**: Retrofitting symbol effects requires restructuring state management. Effects triggered by `value:` changes need the right state architecture from the start.
**Professional response**: "Adding `.symbolEffect(.bounce, value: count)` takes one line. Retrofitting the state to support it later takes a refactor. Let me add the effect now — it's literally one modifier."
### Scenario 3: "Draw animations look janky on our custom symbols"
**Setup**: Custom symbols have Draw animations that look wrong — paths draw in unexpected order or direction.
**Why this matters**: Draw annotation requires intentional guide point placement. Without it, the system guesses and often gets it wrong.
**Fix**: Open custom symbols in SF Symbols 7 app, add guide points explicitly to each path defining start/end/direction. Test each weight variant. See Custom Symbol Draw Annotation section above.
---
## Resources
**WWDC**: 2023-10257, 2023-10258, 2024-10188, 2025-337
**Docs**: /symbols, /symbols/symboleffect, /symbols/symbolrenderingmode, /swiftui/image/symboleffect(_:options:value:), /swiftui/image/symbolrenderingmode(_:)
**Skills**: axiom-sf-symbols-ref, axiom-hig-ref, axiom-swiftui-animation-ref
---
**Last Updated** Based on WWDC 2023/10257-10258, WWDC 2024/10188, WWDC 2025/337
**Version** iOS 17+ (effects), iOS 18+ (Wiggle/Rotate/Breathe), iOS 26+ (Draw On/Off, Variable Draw, Gradients)
This skill helps you select and implement SF Symbols rendering modes, effects, animations, and custom symbol behaviors from iOS 17 through SF Symbols 7/iOS 26. It focuses on practical choices (Monochrome, Hierarchical, Palette, Multicolor), the full symbol effects system (discrete, indefinite, transition, content transition), and the new Draw/Variable Draw and gradient features in iOS 26. Use it to troubleshoot appearance, timing, and cross-platform behavior for xOS apps.
The skill inspects design intent and maps it to rendering modes and symbolEffect APIs so you get the intended visual semantics (depth, brand color, Apple-curated color, or simple tint). It explains when to use discrete vs. indefinite effects, how to configure options like repeat and speed, and how Draw On/Off and Variable Draw work along with playback modes and gradient rendering. It also covers common pitfalls and troubleshooting steps for animations, layer color counts, and platform fallbacks.
Why does my Palette icon look like a single color?
If you pass fewer colors than the symbol has layers, extra layers reuse the last provided color. Provide a color for each layer to avoid this, or use Hierarchical for single-color depth.
My Draw animation doesn't play on device, what should I check?
Draw animations require iOS 26+ and symbols annotated for Draw in SF Symbols 7. Confirm runtime availability, check playback mode, and verify the symbol supports Draw in the SF Symbols app.
Multicolor symbol colors keep getting overridden?
Remove .foregroundColor/.foregroundStyle overrides; Multicolor uses Apple-curated colors and will fall back to Monochrome if unsupported.