Stepper
A native SwiftUI Stepper whose visible label text becomes its accessible name and which VoiceOver increments or decrements automatically, with no .adjustable trait to add.
Selection
The criteria an agent checks before retrieving this component.
Use when
- Use when the user increases or decreases a value in discrete single increments with plus and minus controls (e.g., "Tickets", "Copies", "Quantity"). Uses a native SwiftUI
Stepper, whose increment and decrement behavior is exposed to VoiceOver automatically.
Try a different component when
- Do not use when the value spans a wide continuous range better suited to a thumb slider (use
slider.basic). - Do not use when the choice is among labeled options rather than a number (use
select.menu).
Must Haves
Non-negotiable structure. Every generated instance must satisfy these rules.
- Use a native
Stepperso its increment and decrement behavior is exposed to VoiceOver and operable by Switch Control automatically. SwiftUI has no.adjustabletrait to add; a custom increment control gets its behavior fromaccessibilityAdjustableAction(perglobal.custom-control-representation). - Give the stepper an accessible name from its visible label using the label closure (
Stepper(value: $tickets) { Text("Tickets: \(tickets)") }), whose text becomes the accessible name. A manual.accessibilityLabelis unnecessary in this case.- When the stepper's visible label is empty (e.g., the count is shown in a separate
TextorTextField), add an.accessibilityLabelso the stepper is still named.
- When the stepper's visible label is empty (e.g., the count is shown in a separate
- When the current value is not part of the label text, set
.accessibilityValueso VoiceOver announces the value along with the name. - For a wide range, pair the stepper with a
TextFieldfor direct entry so users are not forced to tap many times; give the paired field the same accessible name. - Give repeated steppers on one screen unique accessible names.
- Meets the touch target size baseline in
global_rules.md(global.touch-target-size). - Meets the system focus indicator baseline in
global_rules.md(global.focus-visible).
Donts
Avoid these accessibility and UX barriers.
- Do not add a
.adjustabletrait; SwiftUI has none (it is a UIKit trait). A nativeStepperprovides the behavior, and a custom control uses.accessibilityAdjustableAction. - Do not ship a
Stepperwith an empty label and no.accessibilityLabel; VoiceOver then announces no name. - Do not force a large value to be reached only by repeated taps; pair a
TextFieldfor direct entry on wide ranges. - Do not leave the value undiscoverable to VoiceOver; put it in the label text or
.accessibilityValue.
Customizable
Alternatives and options that give the AI agent some room to move.
- The value may be carried in the label text (
Text("Tickets: \(tickets)")) or announced through.accessibilityValuewhile the label stays static; either keeps the value discoverable. - A
TextFieldfor direct entry is recommended for wide ranges and optional for small ones. - The increment size (
step:) and bounds (in:) are the engineer's choice for the value being adjusted.
Golden Pattern
The tested reference implementation. Agents start from this shape and adapt to the developer’s codebase and context.
import SwiftUI
struct StepperBasicDemo: View {
@State private var tickets = 1
@State private var copies = 1
var body: some View {
VStack(alignment: .leading, spacing: 16) {
// Visible label text carries the value and becomes the accessible name.
Stepper(value: $tickets, in: 1...10) {
Text("Tickets: \(tickets)")
}
// Wide range: pair a TextField for direct entry; both share the name "Copies".
HStack {
Text("Copies")
TextField("", value: $copies, formatter: NumberFormatter())
.textFieldStyle(.roundedBorder)
.keyboardType(.numberPad)
.accessibilityLabel("Copies")
Stepper("", value: $copies, in: 1...100)
.accessibilityLabel("Copies")
.accessibilityValue("\(copies)")
}
}
}
}
Acceptance Checks
The component’s test spec — an optional body of checks for verification.
Traits & semantics
- The stepper is announced with increment and decrement actions, and no manual
.adjustabletrait is present (SwiftUI has none).
VoiceOver
- The stepper speaks its name from the visible label (or from
.accessibilityLabelwhen the label is empty) and its current value from the label text or.accessibilityValue. - Incrementing or decrementing changes the value and the new value is announced.
- Repeated steppers each speak a distinct name.
Switch Control & Full Keyboard Access
- The stepper's increment and decrement are reachable and operable via Switch Control and a hardware keyboard, and the paired
TextFieldaccepts direct entry from both.
Dynamic Type
- The stepper's label and value text scale with Dynamic Type and stay fully visible.
Stepperdoes not support the Large Content Viewer, so verify legibility through Dynamic Type rather than that magnifier.