```html SwiftUI: How to Build Stepper Input (iOS 17+, 2026)

How to Build a Stepper Input in SwiftUI

iOS 17+ Xcode 16+ Beginner APIs: Stepper Updated: May 11, 2026
TL;DR

Bind an @State integer or double to Stepper, then set the in: range and step: to control increments. SwiftUI handles the +/− buttons and clamping automatically.

struct QuickStepperView: View {
    @State private var quantity = 1

    var body: some View {
        Stepper("Quantity: \(quantity)",
                value: $quantity,
                in: 1...99,
                step: 1)
            .padding()
    }
}

#Preview {
    QuickStepperView()
}

Full implementation

The example below wires a Stepper into a small shopping-cart quantity picker. It clamps the value to a sensible range, displays the current count inside the label closure for full control over formatting, and adds an accessible hint so VoiceOver announces the purpose of the control. A Form wrapper gives the stepper the standard inset-grouped appearance familiar to iOS users.

import SwiftUI

struct QuantityPickerView: View {
    // MARK: – State
    @State private var quantity: Int = 1

    private let range   = 1...50
    private let step    = 1
    private let product = "Espresso Beans (250 g)"

    // MARK: – Body
    var body: some View {
        NavigationStack {
            Form {
                Section("Order details") {
                    Stepper(value: $quantity, in: range, step: step) {
                        HStack {
                            Text("Quantity")
                                .foregroundStyle(.primary)
                            Spacer()
                            Text("\(quantity)")
                                .foregroundStyle(.secondary)
                                .monospacedDigit()
                                .contentTransition(.numericText())
                                .animation(.snappy, value: quantity)
                        }
                    }
                    .accessibilityLabel("Quantity")
                    .accessibilityValue("\(quantity)")
                    .accessibilityHint("Adjust the number of items to order")
                }

                Section {
                    LabeledContent("Item",    value: product)
                    LabeledContent("Each",   value: "$14.99")
                    LabeledContent("Total",  value: totalFormatted)
                        .bold()
                }
            }
            .navigationTitle("Add to Cart")
            .navigationBarTitleDisplayMode(.inline)
        }
    }

    // MARK: – Helpers
    private var totalFormatted: String {
        let total = Double(quantity) * 14.99
        return total.formatted(.currency(code: "USD"))
    }
}

#Preview {
    QuantityPickerView()
}

How it works

  1. Binding with $quantity — Passing the binding to Stepper(value:) means SwiftUI reads the current value and writes increments/decrements automatically. You never touch the +/− logic yourself.
  2. in: range, step: step — The in: parameter is a ClosedRange<Int> that clamps the value so it never goes below 1 or above 50. SwiftUI greys out the appropriate button at the boundary automatically.
  3. Label closure for custom layout — Using the trailing label closure instead of a plain string lets us place an HStack with the count on the right, styled as secondary text with .monospacedDigit() so the layout doesn't jump as digits change.
  4. .contentTransition(.numericText()) — Added to the count Text, this iOS 17+ modifier animates digit changes with a rolling-number effect, giving the control a polished feel without any extra state.
  5. Accessibility modifiers.accessibilityLabel, .accessibilityValue, and .accessibilityHint give VoiceOver users a clear, spoken description of what the stepper does and its current value, meeting WCAG 2.1 AA requirements without duplicating visible text.

Variants

Floating-point steps (e.g. temperature in 0.5° increments)

struct TemperatureStepperView: View {
    @State private var temperature: Double = 20.0

    var body: some View {
        Form {
            Stepper(value: $temperature,
                    in: 15.0...30.0,
                    step: 0.5) {
                LabeledContent("Temperature",
                    value: temperature,
                    format: .number.precision(.fractionLength(1))
                             .notation(.automatic))
            }
        }
        .navigationTitle("Thermostat")
    }
}

#Preview { TemperatureStepperView() }

Callback-based stepper (onIncrement / onDecrement)

When you need side effects on each tap — such as haptic feedback or an async network call — use the Stepper(label:onIncrement:onDecrement:) initialiser instead. Manage the value yourself inside each closure and call UIImpactFeedbackGenerator(style: .light).impactOccurred() to add tactile feedback. Note that clamping is then your responsibility — add guard statements at the top of each closure to stay within your intended range.

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement a stepper input in SwiftUI for iOS 17+.
Use Stepper with a label closure, in: range, and step: parameters.
Make it accessible (VoiceOver labels, accessibilityValue, accessibilityHint).
Add a #Preview with realistic sample data.

In Soarias's Build phase, paste this prompt into the active session — Claude Code will scaffold the component, wire it to your SwiftData model, and drop it straight into the right file without leaving your editor.

Related

FAQ

Does this work on iOS 16?

Yes — Stepper itself has been available since SwiftUI's debut on iOS 13. The one iOS 17-specific feature in the full example is .contentTransition(.numericText()) with an animation; wrap it in if #available(iOS 17, *) and the rest of the view compiles and runs perfectly on iOS 16.

How do I prevent the stepper from going below zero without a range?

Prefer the in: parameter — it's the declarative, safe choice. If you're using the callback form, add guard quantity > 0 else { return } at the top of onDecrement. Avoid clamping after mutation because it causes a brief flash of the out-of-range value before SwiftUI re-renders.

What is the UIKit equivalent?

UIStepper is the direct counterpart. Set minimumValue, maximumValue, and stepValue on it, then observe value changes via a valueChanged action target. In SwiftUI you get all of this in a single Stepper(...) call with no target/action boilerplate.

Last reviewed: 2026-05-11 by the Soarias team.

```