Checkbox
A checkbox built from a SwiftUI Toggle with a custom square toggle style, using accessibilityValue to announce Checked or Unchecked, since SwiftUI has no native checkbox control or trait.
Selection
The criteria an agent checks before retrieving this component.
Use when
- Use when the user selects an independent binary option that is submitted with a form rather than taking effect immediately (e.g., "Accept Terms", "Remember me"). Uses a SwiftUI
Togglewith a custom square.toggleStyle, since SwiftUI has no native checkbox control. - Use when several such options form a group where any number may be selected (e.g., "Preferred contact method(s)": Email, Phone, Text).
Try a different component when
- Do not use when the control is a persistent on/off setting that takes effect immediately (use
switch.basic). - Do not use when the control toggles a feature in place within the current context (use
button.toggle). - Do not use when exactly one option must be chosen from a mutually exclusive set (use
radio.basic).
Must Haves
Non-negotiable structure. Every generated instance must satisfy these rules.
- Build the checkbox from a native
Togglewith a custom.toggleStylethat draws the square box and checkmark (e.g.,squarewhen unchecked,checkmark.squarewhen checked), rather than aButtonor a bare tap gesture, so it keeps the underlying control semantics and stays operable by Switch Control and Full Keyboard Access. - The checkbox has an accessible name matching its visible label; pass the label into the
Toggle(e.g.,Toggle("Accept Terms", isOn: $accepted)or the label closure). - Override the announced value with
.accessibilityValue(isChecked ? "Checked" : "Unchecked")so VoiceOver speaks Checked or Unchecked instead of the default On or Off. - For a checkbox group, label the container: apply
.accessibilityElement(children: .contain)and an.accessibilityLabelmatching the visible group heading, so VoiceOver announces the group name when focus first enters it (WCAG 1.3.1). - The checked and unchecked states are distinguishable by more than color: the box glyph itself changes (empty square versus checkmark), not only a color fill (per
global.semantic-color). - Let the box glyph scale with the label under Dynamic Type; size it with the surrounding font (e.g.,
.imageScale) rather than a fixed point frame that would clip at large sizes. - 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 build a checkbox from a
Buttonthat swaps a checkmark image; VoiceOver then announces "Button" with no Checked or Unchecked value, and the state is invisible to screen reader users. - Do not leave the checkbox announcing the default On or Off; set
.accessibilityValueto Checked or Unchecked. - Do not omit the group label on a set of related checkboxes; without
.accessibilityElement(children: .contain)plus.accessibilityLabel, VoiceOver users do not hear what the group is for. - Do not convey the checked state by color alone; change the box glyph.
- Do not fix the glyph to a static point size that fails to scale with Dynamic Type.
Customizable
Alternatives and options that give the AI agent some room to move.
- The custom
.toggleStylemay draw any recognizable checkbox appearance (square outline, filled check, rounded box) as long as it wraps a realToggle, keeps the state distinguishable without color, and preserves the label and value. - The value wording may match the domain when it reads more clearly than Checked or Unchecked (e.g., "Selected"/"Not selected"), as long as it reflects the binary state and stays in sync with the visible box.
Golden Pattern
The tested reference implementation. Agents start from this shape and adapt to the developer’s codebase and context.
import SwiftUI
struct CheckboxDemo: View {
@State private var acceptedTerms = false
@State private var email = false
@State private var phone = false
@State private var text = false
var body: some View {
VStack(alignment: .leading, spacing: 16) {
// Single checkbox: Toggle + square style; value overridden to Checked/Unchecked
Toggle("Accept Terms", isOn: $acceptedTerms)
.toggleStyle(CheckboxToggleStyle())
.accessibilityValue(acceptedTerms ? "Checked" : "Unchecked")
// Checkbox group: the container carries the group label so VoiceOver
// announces it when focus first enters the group.
VStack(alignment: .leading, spacing: 8) {
Toggle("Email", isOn: $email)
.toggleStyle(CheckboxToggleStyle())
.accessibilityValue(email ? "Checked" : "Unchecked")
Toggle("Phone", isOn: $phone)
.toggleStyle(CheckboxToggleStyle())
.accessibilityValue(phone ? "Checked" : "Unchecked")
Toggle("Text", isOn: $text)
.toggleStyle(CheckboxToggleStyle())
.accessibilityValue(text ? "Checked" : "Unchecked")
}
.accessibilityElement(children: .contain)
.accessibilityLabel("Preferred contact method(s)")
}
}
}
// A real Toggle underneath keeps Switch Control and keyboard operability; the glyph
// change (not color) distinguishes checked from unchecked, and it scales with Dynamic Type.
struct CheckboxToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
Image(systemName: configuration.isOn ? "checkmark.square" : "square")
.imageScale(.large)
configuration.label
}
.contentShape(Rectangle())
.onTapGesture { configuration.isOn.toggle() }
}
}
Acceptance Checks
The component’s test spec — an optional body of checks for verification.
Traits & semantics
- Each checkbox is a single accessible element built on a
Toggle. VoiceOver announces the switch trait (SwiftUI has no native checkbox trait) together with a Checked or Unchecked value. - A checkbox group is a container that announces its group label when focus first enters it.
VoiceOver
- Each checkbox speaks its label and its current value as "Checked" or "Unchecked", not "On" or "Off".
- Double-tapping a checkbox flips it and the announced value updates.
- Moving focus into a group speaks the group label before the first checkbox.
Switch Control & Full Keyboard Access
- Each checkbox is reachable and togglable via Switch Control and a hardware keyboard, with the value change announced.
Dynamic Type
- Checkbox labels scale with Dynamic Type and stay fully visible; the box glyph scales alongside the label rather than clipping.
Visual
- Checked and unchecked states differ by the box glyph (empty square versus checkmark), not by color alone.