--- 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)