Symbol Morph Toggle
A toggle whose SF Symbol morphs between states with the iOS 18 magic replace effect.

- Transitions
- iOS 18.0+
- beginner
- Origin
- Original work
- Found by
- sf symbols · magic replace · symbol effect · toggle · icon animation · morph · replace · símbolos · reemplazo mágico · efecto de símbolo · interruptor · animación de icono · transformación
Description
Section titled “Description”A toggle whose SF Symbol morphs between two states with the magic replace effect. The magic effect shows between related symbols, such as bell.fill and bell.slash.fill.
With Reduce Motion on, the symbols fade.
struct SymbolMorphToggleDemo: View { @State private var alerts = true @State private var microphone = true @State private var camera = true @State private var network = true
var body: some View { Grid(horizontalSpacing: 14, verticalSpacing: 14) { GridRow { SymbolMorphToggle(title: "Alerts", onSymbol: "bell.fill", offSymbol: "bell.slash.fill", isOn: $alerts, tint: .orange) SymbolMorphToggle(title: "Microphone", onSymbol: "mic.fill", offSymbol: "mic.slash.fill", isOn: $microphone, tint: .pink) }
GridRow { SymbolMorphToggle(title: "Camera", onSymbol: "video.fill", offSymbol: "video.slash.fill", isOn: $camera, tint: .green) SymbolMorphToggle(title: "Network", onSymbol: "wifi", offSymbol: "wifi.slash", isOn: $network, tint: .blue) } } .padding(20) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(.systemGroupedBackground).ignoresSafeArea()) }}API reference
Section titled “API reference”| Parameter | Type | Default |
|---|---|---|
title | LocalizedStringResource | Required |
onSymbol | String | Required |
offSymbol | String | Required |
isOn | Binding<Bool> | Required |
tint | Color | .blue |
Copy this code into your project, or download the file.
// SPDX-License-Identifier: MIT// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
/// A toggle whose SF Symbol morphs between two states with the magic replace effect./// The magic effect shows between related symbols, such as `bell.fill` and `bell.slash.fill`.////// With Reduce Motion on, the symbols fade.struct SymbolMorphToggle: View { let title: LocalizedStringResource let onSymbol: String let offSymbol: String @Binding var isOn: Bool var tint: Color = .blue
@ScaledMetric(relativeTo: .largeTitle) private var symbolSize = 44.0 @Environment(\.accessibilityReduceMotion) private var reduceMotion
private var transition: ContentTransition { reduceMotion ? .opacity : .symbolEffect(.replace.magic(fallback: .downUp.byLayer)) }
var body: some View { Button { withAnimation(.smooth(duration: 0.4)) { isOn.toggle() } } label: { VStack(spacing: 12) { Image(systemName: isOn ? onSymbol : offSymbol) .font(.system(size: symbolSize, weight: .medium)) .symbolRenderingMode(.hierarchical) .contentTransition(transition) .frame(height: symbolSize * 1.3) .foregroundStyle(isOn ? tint : .secondary)
Text(title) .font(.headline) } .frame(maxWidth: .infinity) .padding(.vertical, 28) .background(Color(.secondarySystemGroupedBackground), in: .rect(cornerRadius: 24)) .contentShape(.rect) } .buttonStyle(.plain) .accessibilityLabel(title) .accessibilityValue(isOn ? "On" : "Off") .accessibilityAddTraits(.isToggle) }}
// MARK: - Demo
struct SymbolMorphToggleDemo: View { @State private var alerts = true @State private var microphone = true @State private var camera = true @State private var network = true
var body: some View { Grid(horizontalSpacing: 14, verticalSpacing: 14) { GridRow { SymbolMorphToggle(title: "Alerts", onSymbol: "bell.fill", offSymbol: "bell.slash.fill", isOn: $alerts, tint: .orange) SymbolMorphToggle(title: "Microphone", onSymbol: "mic.fill", offSymbol: "mic.slash.fill", isOn: $microphone, tint: .pink) }
GridRow { SymbolMorphToggle(title: "Camera", onSymbol: "video.fill", offSymbol: "video.slash.fill", isOn: $camera, tint: .green) SymbolMorphToggle(title: "Network", onSymbol: "wifi", offSymbol: "wifi.slash", isOn: $network, tint: .blue) } } .padding(20) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(.systemGroupedBackground).ignoresSafeArea()) }}
#Preview { SymbolMorphToggleDemo()}