Button (Toggle)
SwiftUI Button with two states (e.g. pressed/active) conveyed via either an accessible-name change or the .isSelected accessibility trait, never both.
Selection
The criteria an agent checks before retrieving this component.
Use when
- Use when a control toggles a feature or action within the current context (e.g., "Mute", "Bold", "Pin", "Enable Closed Captioning"). Uses a SwiftUI
Buttonwith two states conveyed via either an accessible-name change or the.isSelectedaccessibility trait, never both.
Try a different component when
- Do not use when the control represents a persistent on/off system or application setting, such as "Enable notifications" or "Dark mode" (use
switch.basic). - Do not use when the control drills into a new in-app screen (use
list.row).
Must Haves
Non-negotiable structure. Every generated instance must satisfy these rules.
- Use a native
Buttonfor built-in VoiceOver semantics and Switch Control / external-keyboard activation. - The button has an accessible name that describes its purpose or action.
- Default strategy: represent state by changing the accessible name to the next action (e.g., "Mute" to "Unmute", "Pin" to "Remove Pin") via
.accessibilityLabel. - When the button has visible text and no override is needed, the visible text serves as the accessible name.
- For icon-only toggle buttons, provide an accessible name via
.accessibilityLabelthat reflects the current state's next action. - Keep icons inside a toggle button decorative to VoiceOver; label the button as a whole, not the icon.
- If the action is unavailable, disable the button with
.disabled(true). - 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).
Formatting toolbar exception
- If the control is a formatting toggle in a toolbar (e.g., Bold/Italic/Underline), keep the accessible name stable (e.g., "Bold") and convey the pressed state with
.accessibilityAddTraits(isActive ? .isSelected : [])instead of renaming it. VoiceOver appends "Selected" after the accessible name while the trait is present. - SwiftUI has no "not selected" trait. When the toggle is inactive, omit
.isSelectedrather than adding an antonym trait. Add.accessibilityValue("Not Selected")for the inactive state only when the off state would otherwise be undiscoverable, which holds when all of these are true in the code:- The accessible name is stable across states (the trait strategy, not the name-change strategy), so the name alone never conveys off.
- The control is rendered in isolation, not inside a visually grouped set of peer toggles (e.g., a
Picker, segmented control, or anHStack/toolbar row of sibling formatting toggles) where the selected member is distinguishable by comparison. - No other accessible signal exposes the state: there is no visible value or state text, no
.accessibilityValuealready set, and no surrounding labeled context that implies off. - When any one of these is false, omit
.accessibilityValue; the off state is already discoverable.
Donts
Avoid these accessibility and UX barriers.
- Do not add the
.isSelectedtrait to a toggle button that is also changing its accessible name to the next action. Pick one state model, not both; combining them produces conflicting, confusing announcements like "Unmute, Selected". - Do not leave
.isSelectedstale, always applied, or never applied when using the toolbar trait strategy; it must track the live state. - Do not ship icon-only toggle buttons without an accessible name.
- Do not put state only in the icon's visual appearance. Screen reader users must get the state via the accessible-name change or the
.isSelectedtrait, depending on which strategy is in use.
Customizable
Alternatives and options that give the AI agent some room to move.
- For most toggles (non-toolbar), the "next action" wording may be expressed via visible text (preferred when space allows) and/or
.accessibilityLabel(required for icon-only). - Add context to the accessible name when multiple similar toggles exist on the same screen (e.g., "Mute Trailer", "Unmute Trailer").
- An
.accessibilityHintmay be added when the result of the action is not obvious from the visible label, per the same guidance asbutton.basic.
Golden Pattern
The tested reference implementation. Agents start from this shape and adapt to the developer’s codebase and context.
import SwiftUI
struct ToggleButtonDemo: View {
@State private var muted = false
@State private var pinned = false
@State private var iconOnlyMuted = false
@State private var bold = false
var body: some View {
VStack(spacing: 16) {
// Toggle state indicated by accessible name change
Button(muted ? "Unmute" : "Mute") {
muted.toggle()
}
Button(pinned ? "Remove Pin" : "Pin") {
pinned.toggle()
}
// Icon-only: accessible name itself carries the state
Button {
iconOnlyMuted.toggle()
} label: {
Image(systemName: iconOnlyMuted ? "speaker.slash" : "speaker.wave.2")
}
.accessibilityLabel(iconOnlyMuted ? "Unmute" : "Mute")
// Toggle state indicated by the .isSelected trait (toolbar formatting)
Button {
bold.toggle()
} label: {
Image(systemName: "bold")
}
.accessibilityLabel("Bold")
.accessibilityAddTraits(bold ? .isSelected : [])
}
}
}
Acceptance Checks
The component’s test spec — an optional body of checks for verification.
Traits & semantics
- Each toggle button exposes the button trait. In the toolbar trait strategy, the active state also exposes the selected trait and the inactive state exposes no selected trait.
- A toggle never uses both state models at once: a name-change button never also carries the selected trait, and a trait-strategy button's name never changes when toggled.
- The icon inside a toggle is not surfaced as its own labeled element.
VoiceOver
- Name-change strategy: activating the button changes the spoken name to the next action (e.g., "Mute" becomes "Unmute").
- Trait strategy: the spoken name stays stable (e.g., "Bold") and VoiceOver appends "Selected" only while the control is active.
- Double-tapping performs the toggle and the announced state updates.
Switch Control & Full Keyboard Access
- The toggle is reachable and activatable via Switch Control and a hardware keyboard, with the state change conveyed the same way as under VoiceOver.
Dynamic Type
- Any visible toggle text scales with Dynamic Type and stays fully visible at accessibility text sizes.