Skip to main content
iOS / SwiftUI · WCAG 2.2 AA

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 Stepper so its increment and decrement behavior is exposed to VoiceOver and operable by Switch Control automatically. SwiftUI has no .adjustable trait to add; a custom increment control gets its behavior from accessibilityAdjustableAction (per global.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 .accessibilityLabel is unnecessary in this case.
    • When the stepper's visible label is empty (e.g., the count is shown in a separate Text or TextField), add an .accessibilityLabel so the stepper is still named.
  • When the current value is not part of the label text, set .accessibilityValue so VoiceOver announces the value along with the name.
  • For a wide range, pair the stepper with a TextField for 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 .adjustable trait; SwiftUI has none (it is a UIKit trait). A native Stepper provides the behavior, and a custom control uses .accessibilityAdjustableAction.
  • Do not ship a Stepper with 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 TextField for 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 .accessibilityValue while the label stays static; either keeps the value discoverable.
  • A TextField for 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.

SWIFT
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 .adjustable trait is present (SwiftUI has none).

VoiceOver

  • The stepper speaks its name from the visible label (or from .accessibilityLabel when 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 TextField accepts direct entry from both.

Dynamic Type

  • The stepper's label and value text scale with Dynamic Type and stay fully visible. Stepper does not support the Large Content Viewer, so verify legibility through Dynamic Type rather than that magnifier.