Spring Toggle Style
A custom toggle style whose knob widens under the finger and overshoots as it lands.

- Controls
- iOS 18.0+
- beginner
- Origin
- Original work
- Found by
- toggle · switch · toggle style · spring · knob · settings · bouncy · interruptor · conmutador · estilo de toggle · muelle · rebote · ajustes
Description
Section titled “Description”A toggle whose knob widens under your finger and overshoots as it lands. The whole row is tappable, and VoiceOver reads it as a standard switch.
With Reduce Motion on, the knob slides without bouncing or widening.
struct SpringToggleStyleDemo: View { @State private var notifications = false @State private var sounds = true @State private var haptics = false
var body: some View { VStack(spacing: 28) { Toggle(isOn: $notifications) { Label("Notifications", systemImage: "bell.fill") } Toggle(isOn: $sounds) { Label("Sounds", systemImage: "speaker.wave.2.fill") } Toggle(isOn: $haptics) { Label("Haptics", systemImage: "iphone.radiowaves.left.and.right") } } .toggleStyle(.spring) .font(.title3.weight(.medium)) .padding(24) .background(.background, in: .rect(cornerRadius: 28)) .padding(20) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(uiColor: .systemGroupedBackground).ignoresSafeArea()) }}API reference
Section titled “API reference”| Parameter | Type | Default |
|---|---|---|
onColor | 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 toggle whose knob widens under your finger and overshoots as it lands./// The whole row is tappable, and VoiceOver reads it as a standard switch.////// With Reduce Motion on, the knob slides without bouncing or widening.struct SpringToggleStyle: ToggleStyle { var onColor: Color = .green
func makeBody(configuration: Configuration) -> some View { Button { configuration.isOn.toggle() } label: { configuration.label .frame(maxWidth: .infinity, alignment: .leading) .contentShape(.rect) } .buttonStyle(SpringToggleStyleRow(isOn: configuration.isOn, onColor: onColor)) .accessibilityRepresentation { Toggle(isOn: configuration.$isOn) { configuration.label } } }}
extension ToggleStyle where Self == SpringToggleStyle { static var spring: SpringToggleStyle { SpringToggleStyle() }}
private struct SpringToggleStyleRow: ButtonStyle { let isOn: Bool let onColor: Color
@ScaledMetric(relativeTo: .body) private var trackHeight = 34.0 @Environment(\.accessibilityReduceMotion) private var reduceMotion
private let knobInset = 3.0
private var animation: Animation { reduceMotion ? .easeInOut(duration: 0.15) : .spring(duration: 0.45, bounce: 0.55) }
func makeBody(configuration: Configuration) -> some View { let isSquashed = configuration.isPressed && !reduceMotion let knobHeight = trackHeight - knobInset * 2
HStack(spacing: 16) { configuration.label
Capsule() .fill(isOn ? onColor : Color.gray.opacity(0.35)) .frame(width: trackHeight * 1.75, height: trackHeight) .overlay(alignment: isOn ? .trailing : .leading) { Capsule() .fill(.white) .shadow(color: .black.opacity(0.25), radius: 3, y: 2) .frame(width: isSquashed ? knobHeight * 1.3 : knobHeight, height: knobHeight) .padding(knobInset) } } .animation(animation, value: isOn) .animation(animation, value: isSquashed) }}
// MARK: - Demo
struct SpringToggleStyleDemo: View { @State private var notifications = false @State private var sounds = true @State private var haptics = false
var body: some View { VStack(spacing: 28) { Toggle(isOn: $notifications) { Label("Notifications", systemImage: "bell.fill") } Toggle(isOn: $sounds) { Label("Sounds", systemImage: "speaker.wave.2.fill") } Toggle(isOn: $haptics) { Label("Haptics", systemImage: "iphone.radiowaves.left.and.right") } } .toggleStyle(.spring) .font(.title3.weight(.medium)) .padding(24) .background(.background, in: .rect(cornerRadius: 28)) .padding(20) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(uiColor: .systemGroupedBackground).ignoresSafeArea()) }}
#Preview { SpringToggleStyleDemo()}