Sliding Segmented Picker
A segmented picker whose selected pill glides between the options.

- Controls
- iOS 18.0+
- beginner
- Origin
- Original work
- Found by
- segmented control · picker · tabs · sliding pill · selection · matched geometry · control segmentado · selector · pestañas · píldora deslizante · selección · geometría compartida
Description
Section titled “Description”A segmented picker whose selected pill glides between the options.
With Reduce Motion on, the pill moves without animating.
struct SlidingSegmentedPickerDemo: View { private enum Period: String, CaseIterable { case day = "Day" case week = "Week" case month = "Month" case year = "Year"
var steps: Int { switch self { case .day: 8_240 case .week: 54_910 case .month: 231_075 case .year: 2_904_330 } } }
@State private var period = Period.day @Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View { VStack(spacing: 36) { VStack(spacing: 6) { Text(period.steps, format: .number) .font(.system(.largeTitle, design: .rounded, weight: .bold)) .contentTransition(reduceMotion ? .identity : .numericText(value: Double(period.steps))) Text("steps walked") .font(.headline) .foregroundStyle(.secondary) } .accessibilityElement(children: .combine)
SlidingSegmentedPicker(selection: $period, options: Period.allCases) { $0.rawValue } } .animation(reduceMotion ? nil : .snappy, value: period) .padding(24) .frame(maxWidth: .infinity, maxHeight: .infinity) }}API reference
Section titled “API reference”| Parameter | Type | Default |
|---|---|---|
selection | Binding<Option> | Required |
options | [Option] | Required |
tint | Color | .indigo |
title | (Option) -> String | Required |
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 segmented picker whose selected pill glides between the options.////// With Reduce Motion on, the pill moves without animating.struct SlidingSegmentedPicker<Option: Hashable>: View { @Binding var selection: Option let options: [Option] var tint: Color = .indigo let title: (Option) -> String
@Namespace private var namespace @Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View { HStack(spacing: 4) { ForEach(options, id: \.self) { option in let isSelected = option == selection
Button { selection = option } label: { Text(title(option)) .font(.headline) .lineLimit(1) .minimumScaleFactor(0.7) .foregroundStyle(isSelected ? Color.white : Color.primary) .frame(maxWidth: .infinity) .padding(.vertical, 14) .background { if isSelected { Capsule() .fill(tint.gradient) .matchedGeometryEffect(id: "pill", in: namespace) } } .contentShape(.capsule) } .buttonStyle(.plain) .accessibilityAddTraits(isSelected ? .isSelected : []) } } .padding(4) .background(.quaternary, in: .capsule) .animation(reduceMotion ? nil : .spring(duration: 0.4, bounce: 0.25), value: selection) }}
// MARK: - Demo
struct SlidingSegmentedPickerDemo: View { private enum Period: String, CaseIterable { case day = "Day" case week = "Week" case month = "Month" case year = "Year"
var steps: Int { switch self { case .day: 8_240 case .week: 54_910 case .month: 231_075 case .year: 2_904_330 } } }
@State private var period = Period.day @Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View { VStack(spacing: 36) { VStack(spacing: 6) { Text(period.steps, format: .number) .font(.system(.largeTitle, design: .rounded, weight: .bold)) .contentTransition(reduceMotion ? .identity : .numericText(value: Double(period.steps))) Text("steps walked") .font(.headline) .foregroundStyle(.secondary) } .accessibilityElement(children: .combine)
SlidingSegmentedPicker(selection: $period, options: Period.allCases) { $0.rawValue } } .animation(reduceMotion ? nil : .snappy, value: period) .padding(24) .frame(maxWidth: .infinity, maxHeight: .infinity) }}
#Preview { SlidingSegmentedPickerDemo()}