Skip to content
Components PulseSegmentedControl

PulseSegmentedControl

Selection with a precise native spring.

Form
interactive demo
OverviewActivityInsights

A live browser stand-in using the exact tokens, motion and API of the SwiftUI component.

Selection with a precise native spring.

A themed segmented control that keeps the functional glass treatment restrained and accessible.

Generic Hashable selection
Native button semantics
Reduce Motion aware
Adaptive labels
Usage
PulseSegmentedControl(selection: $selection, items: tabs)
Install
$ pulse add segmented_control

Source files

Sources/PulseUI/Components/PulseSegmentedControl/PulseSegmentedControl.swift
Full source Sources/PulseUI/Components/PulseSegmentedControl/PulseSegmentedControl.swift
import SwiftUI

// MARK: - PulseSegmentedControl

/// A native-feeling segmented control with keyboard and VoiceOver semantics.
public struct PulseSegmentedControl<Item: Hashable>: View {
    @Environment(\.pulseTheme) private var theme
    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    @Binding private var selection: Item
    private let items: [Item]
    private let title: (Item) -> String

    public init(
        selection: Binding<Item>,
        items: [Item],
        title: @escaping (Item) -> String = { String(describing: $0) }
    ) {
        self._selection = selection
        self.items = items
        self.title = title
    }

    public var body: some View {
        HStack(spacing: 3) {
            ForEach(items, id: \.self) { item in
                Button {
                    withAnimation(reduceMotion ? .none : theme.motion.springSnappy) {
                        selection = item
                    }
                } label: {
                    Text(title(item))
                        .font(theme.typography.caption.font.weight(.semibold))
                        .foregroundStyle(
                            selection == item
                                ? theme.colors.foreground
                                : theme.colors.foregroundSecondary
                        )
                        .frame(maxWidth: .infinity, minHeight: 36)
                        .background {
                            if selection == item {
                                RoundedRectangle(cornerRadius: theme.radius.md, style: .continuous)
                                    .fill(theme.colors.card)
                                    .shadow(color: .black.opacity(0.08), radius: 5, y: 2)
                                    .transition(.opacity)
                            }
                        }
                }
                .buttonStyle(.plain)
                .accessibilityAddTraits(selection == item ? [.isSelected] : [])
                .accessibilityLabel(title(item))
                .accessibilityHint("Double tap to select")
            }
        }
        .padding(3)
        .background(theme.colors.backgroundSecondary.opacity(0.82))
        .clipShape(RoundedRectangle(cornerRadius: theme.radius.lg, style: .continuous))
        .pulseGlass(cornerRadius: theme.radius.lg)
        .accessibilityElement(children: .contain)
        .accessibilityLabel("Segmented control")
    }
}