| name | ios-accessibility |
| description | iOS accessibility (a11y) expert skill covering VoiceOver support (labels, hints, values, traits, custom actions, rotors, focus management), Dynamic Type (text styles, @ScaledMetric, layout adaptation for large sizes), color and contrast (WCAG ratios, Differentiate Without Color, Smart Invert), motion and reduce motion, accessibility auditing (Xcode Inspector, XCTest performAccessibilityAudit), and SwiftUI accessibility modifiers. Use this skill whenever the user implements accessibility features, needs VoiceOver support, handles Dynamic Type, adds accessibility labels, or audits an app for a11y compliance. Triggers on: accessibility, a11y, VoiceOver, Dynamic Type, accessibilityLabel, accessibilityHint, accessibilityValue, accessibilityTraits, accessibilityAction, accessibilityIdentifier, screen reader, assistive technology, reduce motion, high contrast, accessible, WCAG, touch target, font scaling, @ScaledMetric, accessibilityElement, AX audit, inclusive design, or any iOS accessibility question.
|
iOS Accessibility Skill
Core Rules
-
EVERY interactive element must have a meaningful accessibilityLabel.
Never use generic labels like "button", "image", or "icon". Describe what it does: "Delete message", "Add to favorites", "Share photo".
-
Use native SwiftUI controls (Button, Toggle, Link, Picker, Slider, Stepper) whenever possible. They carry correct accessibility traits automatically.
-
Custom views with onTapGesture MUST add .accessibilityAddTraits(.isButton).
Better yet, wrap them in a Button so VoiceOver announces them as interactive.
-
Decorative images must be hidden from VoiceOver.
Use Image(decorative:) or .accessibilityHidden(true). Never let VoiceOver read "image" for decorative content.
-
Support Dynamic Type at ALL sizes including accessibility sizes (AX1-AX5). Use system text styles (.body, .title, .headline) and @ScaledMetric for custom dimensions.
-
Minimum touch target: 44x44 points. Use .frame(minWidth: 44, minHeight: 44) and .contentShape(.rect) to expand small icons.
-
Never re-prompt after .userCancel. When the user dismisses a biometric prompt or permission dialog, respect their decision. Do not show the prompt again immediately.
-
Test with VoiceOver on a real device, not just the Simulator. The Simulator does not fully replicate VoiceOver behavior, gestures, or focus management.
-
Run performAccessibilityAudit() in UI tests. Automated audits catch contrast issues, missing labels, small hit regions, and clipped text.
-
Add NSFaceIDUsageDescription to Info.plist when using Face ID. Without it the app crashes on first biometric attempt.
-
Color must not be the sole indicator. Always pair color with text, icons, or shapes. Check accessibilityDifferentiateWithoutColor.
-
Respect Reduce Motion. Check accessibilityReduceMotion and provide crossfade alternatives to spring/slide animations.
-
Logical reading order matters. VoiceOver reads left-to-right, top-to-bottom. Use .accessibilitySortPriority() to fix order when layout does not match logical flow.
-
Move focus to important changes. When an error appears or content updates, post AccessibilityNotification.LayoutChanged or use @AccessibilityFocusState.
-
Group related elements with .accessibilityElement(children: .combine) to reduce swipe count and create meaningful compound descriptions.
Quick Checklist
Before shipping any screen, verify:
SwiftUI Accessibility Modifier Quick Reference
Labels, Hints, Values
.accessibilityLabel("Delete message")
.accessibilityLabel { label in
Text("Unread") + label
}
.accessibilityHint("Double tap to delete this message")
.accessibilityValue("50 percent")
.accessibilityInputLabels(["Delete", "Remove", "Trash"])
Traits
.accessibilityAddTraits(.isButton)
.accessibilityAddTraits(.isHeader)
.accessibilityAddTraits(.isLink)
.accessibilityAddTraits(.isSelected)
.accessibilityAddTraits(.isImage)
.accessibilityRemoveTraits(.isImage)
Grouping and Hiding
.accessibilityElement(children: .combine)
.accessibilityHidden(true)
Image(decorative: "background-pattern")
.accessibilityRepresentation {
Toggle("Wi-Fi", isOn: $isEnabled)
}
Actions
.accessibilityAction(named: "Mark as read") {
markAsRead()
}
.accessibilityAdjustableAction { direction in
switch direction {
case .increment: value += 1
case .decrement: value -= 1
@unknown default: break
}
}
Focus Management
@AccessibilityFocusState private var isFocused: Bool
TextField("Name", text: $name)
.accessibilityFocused($isFocused)
Button("Show Error") {
errorMessage = "Invalid input"
isFocused = true
}
Dynamic Type Support
Text("Hello").font(.body)
Text("Hello").font(.custom("Avenir", size: 17, relativeTo: .body))
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = 24
@Environment(\.dynamicTypeSize) private var typeSize
if typeSize.isAccessibilitySize {
}
Common Patterns
Card with Combined Accessibility
VStack(alignment: .leading) {
Text(item.title).font(.headline)
Text(item.subtitle).font(.subheadline)
Text(item.date).font(.caption)
}
.accessibilityElement(children: .combine)
.accessibilityAddTraits(.isButton)
.accessibilityHint("Double tap to view details")
Custom Toggle
HStack {
Text("Dark Mode")
Image(systemName: isDark ? "moon.fill" : "moon")
}
.onTapGesture { isDark.toggle() }
Toggle("Dark Mode", isOn: $isDark)
HStack {
Text("Dark Mode")
Image(systemName: isDark ? "moon.fill" : "moon")
}
.onTapGesture { isDark.toggle() }
.accessibilityRepresentation {
Toggle("Dark Mode", isOn: $isDark)
}
Swipeable List Row with Actions
ForEach(messages) { message in
MessageRow(message: message)
.accessibilityAction(named: "Delete") {
delete(message)
}
.accessibilityAction(named: "Archive") {
archive(message)
}
.accessibilityAction(named: "Mark as read") {
markAsRead(message)
}
}
Error Announcement
@AccessibilityFocusState private var isErrorFocused: Bool
VStack {
TextField("Email", text: $email)
if let error = validationError {
Text(error)
.foregroundStyle(.red)
.accessibilityFocused($isErrorFocused)
}
Button("Submit") {
if !validate() {
isErrorFocused = true
}
}
}
Responsive Layout for Large Type
struct AdaptiveRow: View {
@Environment(\.dynamicTypeSize) private var typeSize
var body: some View {
let layout = typeSize.isAccessibilitySize
? AnyLayout(VStackLayout(alignment: .leading, spacing: 8))
: AnyLayout(HStackLayout(spacing: 12))
layout {
Image(systemName: "star.fill")
.accessibilityHidden(true)
Text("Favorites")
Spacer()
Text("12 items")
.foregroundStyle(.secondary)
}
}
}
References
For detailed API coverage, see:
- VoiceOver — labels, hints, values, traits, actions, rotors, focus
- Dynamic Type — text styles, @ScaledMetric, layout adaptation, color and contrast, motion
- Auditing — Xcode Inspector, XCTest audits, environment values, best practices