Icon Switch Toggle Style
A switch toggle style whose knob carries a symbol for each state, like a checkmark and a cross, that swaps as it slides.
- Controls
- iOS 18.0+
- beginner
- Origin
- Original work
- Found by
- switch · toggle · toggle style · icon · symbol · knob · on off · settings · interruptor · toggle · estilo de toggle · icono · símbolo · encendido apagado · ajustes
Description
Section titled “Description”A switch whose knob carries a symbol for each state, a checkmark and a cross by default, that swaps as the knob slides. The state reads without relying on the track color alone. The whole row is tappable, and VoiceOver reads it as a standard switch.
With Reduce Motion on, the knob slides without bouncing and the symbols crossfade.
struct IconSwitchToggleStyleDemo: View { @State private var autoUpdates = true @State private var darkMode = false @State private var sound = true @State private var location = false
var body: some View { VStack(spacing: 8) { Toggle("Automatic Updates", isOn: $autoUpdates) .toggleStyle(.iconSwitch) Toggle("Dark Mode", isOn: $darkMode) .toggleStyle(.iconSwitch(on: "moon.fill", off: "sun.max.fill", color: .indigo)) Toggle("Sound", isOn: $sound) .toggleStyle(.iconSwitch(on: "speaker.wave.2.fill", off: "speaker.slash.fill", color: .orange)) Toggle("Share Location", isOn: $location) .toggleStyle(.iconSwitch(on: "location.fill", off: "location.slash.fill", color: .blue)) } .font(.body.weight(.medium)) .padding(.horizontal, 20) .padding(.vertical, 12) .background(Color(uiColor: .secondarySystemGroupedBackground), in: .rect(cornerRadius: 24, style: .continuous)) .padding(20) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(uiColor: .systemGroupedBackground).ignoresSafeArea()) }}API reference
Section titled “API reference”| Parameter | Type | Default |
|---|---|---|
onSymbol | String | "checkmark" |
offSymbol | String | "xmark" |
color | Color | .green |
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 switch whose knob carries a symbol for each state, a checkmark and a cross/// by default, that swaps as the knob slides. The state reads without relying/// on the track color alone. The whole row is tappable, and VoiceOver reads it/// as a standard switch.////// With Reduce Motion on, the knob slides without bouncing and the symbols crossfade.struct IconSwitchToggleStyle: ToggleStyle { var onSymbol = "checkmark" var offSymbol = "xmark" var color: Color = .green
func makeBody(configuration: Configuration) -> some View { Button { configuration.isOn.toggle() } label: { HStack(spacing: 16) { configuration.label .frame(maxWidth: .infinity, alignment: .leading) IconSwitchToggleStyleTrack( isOn: configuration.isOn, symbol: configuration.isOn ? onSymbol : offSymbol, color: color ) } .frame(minHeight: 44) .contentShape(.rect) } .buttonStyle(.plain) .accessibilityRepresentation { Toggle(isOn: configuration.$isOn) { configuration.label } } }}
extension ToggleStyle where Self == IconSwitchToggleStyle { static var iconSwitch: IconSwitchToggleStyle { IconSwitchToggleStyle() }
static func iconSwitch(on onSymbol: String, off offSymbol: String, color: Color = .green) -> IconSwitchToggleStyle { IconSwitchToggleStyle(onSymbol: onSymbol, offSymbol: offSymbol, color: color) }}
private struct IconSwitchToggleStyleTrack: View { let isOn: Bool let symbol: String let color: Color
@ScaledMetric(relativeTo: .body) private var height = 32.0 @Environment(\.isEnabled) private var isEnabled @Environment(\.accessibilityReduceMotion) private var reduceMotion
private let inset = 3.0
var body: some View { let knob = height - inset * 2
Capsule() .fill(isOn ? color : Color.secondary.opacity(0.35)) .frame(width: height * 1.8, height: height) .overlay(alignment: isOn ? .trailing : .leading) { Circle() .fill(.white) .shadow(color: .black.opacity(0.2), radius: 2, y: 1) .overlay { Image(systemName: symbol) .font(.system(size: knob * 0.45, weight: .bold)) .foregroundStyle(isOn ? color : Color.gray) .contentTransition(reduceMotion ? .opacity : .symbolEffect(.replace)) } .frame(width: knob, height: knob) .padding(inset) } .opacity(isEnabled ? 1 : 0.4) .animation(reduceMotion ? .easeInOut(duration: 0.15) : .spring(duration: 0.35, bounce: 0.35), value: isOn) .accessibilityHidden(true) }}
// MARK: - Demo
struct IconSwitchToggleStyleDemo: View { @State private var autoUpdates = true @State private var darkMode = false @State private var sound = true @State private var location = false
var body: some View { VStack(spacing: 8) { Toggle("Automatic Updates", isOn: $autoUpdates) .toggleStyle(.iconSwitch) Toggle("Dark Mode", isOn: $darkMode) .toggleStyle(.iconSwitch(on: "moon.fill", off: "sun.max.fill", color: .indigo)) Toggle("Sound", isOn: $sound) .toggleStyle(.iconSwitch(on: "speaker.wave.2.fill", off: "speaker.slash.fill", color: .orange)) Toggle("Share Location", isOn: $location) .toggleStyle(.iconSwitch(on: "location.fill", off: "location.slash.fill", color: .blue)) } .font(.body.weight(.medium)) .padding(.horizontal, 20) .padding(.vertical, 12) .background(Color(uiColor: .secondarySystemGroupedBackground), in: .rect(cornerRadius: 24, style: .continuous)) .padding(20) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(uiColor: .systemGroupedBackground).ignoresSafeArea()) }}
#Preview { IconSwitchToggleStyleDemo()}