Skip to content

Spring Toggle Style

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

Spring Toggle Style, playing
Download .swift
Origin
Original work
Found by
toggle · switch · toggle style · spring · knob · settings · bouncy · interruptor · conmutador · estilo de toggle · muelle · rebote · ajustes

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())
}
}
ParameterTypeDefault
onColorColor.green

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

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