SwiftUI Slider Component
A slider that grows under your thumb and ticks haptically as it crosses steps. The track thickens on contact — the same trick that makes the system volume slider feel physical.
Volume28
Brightness72
Variants: Continuous, Stepped
Install the free SwiftUI slider
- Add SwiftKitUI.swift to your app once. It holds the tokens and motion curves.
- Copy SKSlider.swift below into your project. It needs nothing else.
Usage
Usage.swift
SKSlider(value: $volume, in: 0...1)
SKSlider(value: $rating, in: 0...10, step: 1) { Text("Rating") }Full source · 136 lines · MIT
SKSlider.swift
//
// SKSlider.swift
// Swift Kit UI
//
// Requires: SwiftKitUI.swift
//
// A slider that grows under your thumb and ticks haptically as it crosses
// steps. The track thickens on contact — the same trick that makes the system
// volume slider feel physical.
//
// Usage:
// SKSlider(value: $volume, in: 0...1)
// SKSlider(value: $rating, in: 0...10, step: 1) { Text("Rating") }
//
import SwiftUI
public struct SKSlider<Label: View>: View {
@Binding private var value: Double
private let range: ClosedRange<Double>
private let step: Double?
private let label: Label
@State private var isDragging = false
@State private var lastTick: Double?
@Environment(\.isEnabled) private var isEnabled
public init(
value: Binding<Double>,
in range: ClosedRange<Double> = 0...1,
step: Double? = nil,
@ViewBuilder label: () -> Label
) {
self._value = value
self.range = range
self.step = step
self.label = label()
}
private var progress: Double {
let span = range.upperBound - range.lowerBound
guard span > 0 else { return 0 }
return min(max((value - range.lowerBound) / span, 0), 1)
}
private var trackHeight: CGFloat { isDragging ? 14 : 10 }
public var body: some View {
VStack(alignment: .leading, spacing: SK.Space.sm) {
label
GeometryReader { geo in
track(width: geo.size.width)
.frame(height: SK.Icon.hitTarget)
.contentShape(.rect)
.gesture(drag(width: geo.size.width))
}
.frame(height: SK.Icon.hitTarget)
}
.opacity(isEnabled ? 1 : 0.45)
.skAnimation(.micro, value: isDragging)
.accessibilityElement()
.accessibilityValue(Text(String(format: "%.2f", value)))
.accessibilityAdjustableAction { direction in
let delta = step ?? (range.upperBound - range.lowerBound) / 20
switch direction {
case .increment: value = min(value + delta, range.upperBound)
case .decrement: value = max(value - delta, range.lowerBound)
@unknown default: break
}
}
}
private func track(width: CGFloat) -> some View {
ZStack(alignment: .leading) {
Capsule().fill(SK.Palette.fill)
.frame(height: trackHeight)
Capsule().fill(SK.Palette.accent)
.frame(width: max(trackHeight, width * progress), height: trackHeight)
Circle()
.fill(.white)
.shadow(color: .black.opacity(0.22), radius: 4, y: 2)
.frame(width: isDragging ? 30 : 26, height: isDragging ? 30 : 26)
.offset(x: (width - 26) * progress)
}
.frame(maxHeight: .infinity)
}
private func drag(width: CGFloat) -> some Gesture {
DragGesture(minimumDistance: 0)
.onChanged { g in
isDragging = true
let span = range.upperBound - range.lowerBound
let raw = range.lowerBound + Double(min(max(g.location.x / width, 0), 1)) * span
let next = step.map { (raw / $0).rounded() * $0 } ?? raw
let clamped = min(max(next, range.lowerBound), range.upperBound)
// Tick only when a step boundary is actually crossed, otherwise
// a continuous drag would fire haptics every frame.
if step != nil, clamped != lastTick {
SK.Haptic.play(.light)
lastTick = clamped
}
value = clamped
}
.onEnded { _ in
isDragging = false
lastTick = nil
}
}
}
extension SKSlider where Label == EmptyView {
public init(value: Binding<Double>, in range: ClosedRange<Double> = 0...1, step: Double? = nil) {
self.init(value: value, in: range, step: step) { EmptyView() }
}
}
#Preview("SKSlider") {
@Previewable @State var volume = 0.4
@Previewable @State var rating = 6.0
VStack(spacing: SK.Space.xl) {
SKSlider(value: $volume) {
Text("Volume").font(SK.Type_.callout).foregroundStyle(SK.Palette.labelSecondary)
}
SKSlider(value: $rating, in: 0...10, step: 1) {
Text("Steps of 1 — feel the ticks")
.font(SK.Type_.callout).foregroundStyle(SK.Palette.labelSecondary)
}
SKSlider(value: .constant(0.6)).disabled(true)
}
.padding(SK.Space.xl)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(SK.Palette.surfaceSunken)
}
Questions
- Is this SwiftUI slider free for commercial apps?
- Yes. Swift Kit UI is MIT licensed, so you can use it in personal and commercial apps with no payment or sign-up. Keep the licence notice with the code.
- What does it depend on?
- SKSlider.swift depends only on SwiftKitUI.swift, a single foundation file with the design tokens and motion curves. There are no third-party packages.
- Which iOS versions does it support?
- iOS 17 and later, plus macOS 14.
Related components
Building a whole app? The Swift Kit adds auth, payments and onboarding, ready to ship.