Skip to content

Symbol Morph Toggle

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

Symbol Morph Toggle, playing
Download .swift
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

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())
}
}
ParameterTypeDefault
titleLocalizedStringResourceRequired
onSymbolStringRequired
offSymbolStringRequired
isOnBinding<Bool>Required
tintColor.blue

Copy this code into your project, or download the file.

SymbolMorphToggle.swift
// 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()
}