iOS Accessibility
Build and audit SwiftUI, UIKit, and AppKit accessibility for VoiceOver, Voice Control, Switch Control, Full Keyboard Access, Dynamic Type, focus restoration, labels/traits/actions, traversal, custom rotors, NSAccessibility, XCTest checks, adaptive system preferences, and App Store accessibility declarations. Use when implementing accessible UI, fixing an accessibility audit, testing assistive-technology behavior, or substantiating App Store Accessibility Nutrition Labels.
- Skill ID
- dpearson2699/swift-ios-skills/ios-accessibility
- Publisher
- dpearson2699
- Repository
- swift-ios-skills
- Installs
- 0
- Files
- 5
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
dpearson2699/swift-ios-skills/ios-accessibilityInstalls these files- SKILL.md
- evals/evals.json
- references/a11y-patterns.md
- references/media-accessibility.md
- references/nutrition-labels.md
What this skill tells the agent
iOS/macOS Accessibility - SwiftUI, UIKit, and AppKit
Build SwiftUI, UIKit, and AppKit interfaces that remain operable with VoiceOver, Switch Control, Voice Control, Full Keyboard Access, and adaptive accessibility settings.
Contents
- Core Principles
- How VoiceOver Reads Elements
- SwiftUI Accessibility Modifiers
- Focus Management
- Dynamic Type
- Custom Rotors
- System Accessibility Preferences
- Decorative Content
- Voice Control
- Switch Control
- Full Keyboard Access
- Assistive Access (iOS 18+)
- UIKit Accessibility Patterns
- AppKit Accessibility Patterns
- Accessibility Custom Content
- App Store Accessibility Nutrition Labels
- Testing Accessibility
- Common Mistakes
- Review Checklist
- References
Core Principles
- Prefer semantic system controls; custom controls must expose the same name, role, value, state, and actions.
- Preserve every task across assistive input: do not make a gesture, color, motion, hover state, or visual arrangement the only path to meaning or action.
- Keep focus and traversal intentional across navigation, presentation, updates, and dismissal.
- Let text, layout, contrast, transparency, and motion adapt to the user's accessibility settings.
- Treat App Store accessibility declarations as evidence-backed product claims, not as a summary of isolated control-level support.
How VoiceOver Reads Elements
VoiceOver reads element properties in a fixed, non-configurable order:
Label -> Value -> Trait -> Hint
Design your labels, values, and hints with this reading order in mind.
SwiftUI Accessibility Modifiers
See references/a11y-patterns.md for detailed SwiftUI modifier examples (labels, hints, traits, grouping, custom controls, adjustable actions, and custom actions).
Focus Management
Focus management is where most apps fail. When a sheet, alert, or popover is dismissed, VoiceOver focus MUST return to the element that triggered it.
This section is about accessibility focus for assistive technologies. For keyboard focus, directional focus, focusSection(), scene-focused values, and UIFocusGuide, use the focus-engine skill.
Audit element order and grouping in Traversal Order; route keyboard-focus mechanics to focus-engine.
@AccessibilityFocusState (iOS 15+)
@AccessibilityFocusState is a property wrapper that reads and writes the current accessibility focus. It works with Bool for single-target focus or an optional Hashable enum for multi-target focus.
struct ContentView: View {
@State private var showSheet = false
@AccessibilityFocusState private var focusOnTrigger: Bool
var body: some View {
Button("Open Settings") { showSheet = true }
.accessibilityFocused($focusOnTrigger)
.sheet(isPresented: $showSheet) {
SettingsSheet()
.onDisappear {
// Slight delay allows the transition to complete before moving focus
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(100))
focusOnTrigger = true
}
}
}
}
}Multi-Target Focus with Enum
enum A11yFocus: Hashable {
case nameField
case emailField
case submitButton
}
struct FormView: View {
@AccessibilityFocusState private var focus: A11yFocus?
var body: some View {
Form {
TextField("Name", text: $name)
.accessibilityFocused($focus, equals: .nameField)
TextField("Email", text: $email)
.accessibilityFocused($focus, equals: .emailField)
Button("Submit") { validate() }
.accessibilityFocused($focus, equals: .submitButton)
}
}
func validate() {
if name.isEmpty {
focus = .nameField // Move VoiceOver to the invalid field
}
}
}Custom Modals
Custom overlay views need the .isModal trait to trap VoiceOver focus and an escape action for dismissal:
CustomDialog()
.accessibilityAddTraits(.isModal)
.accessibilityAction(.escape) { dismiss() }Test dismissal as part of the modal contract: users must be able to dismiss the overlay with the relevant assistive-technology escape gesture or keyboard escape path, and focus should return to the trigger or next logical target.
Accessibility Notifications (UIKit)
When you need to announce changes or move focus imperatively in UIKit contexts:
// Announce a status change (e.g., "Item deleted", "Upload complete")
UIAccessibility.post(notification: .announcement, argument: "Upload complete")
// Partial screen update -- move focus to a specific element
UIAccessibility.post(notification: .layoutChanged, argument: targetView)
// Full screen transition -- move focus to the new screen
UIAccessibility.post(notification: .screenChanged, argument: newScreenView)Dynamic Type
Scale text with system text styles. Scale non-text dimensions too: icon sizes, spacing, control heights, and custom hit-region dimensions should use @ScaledMetric(relativeTo:) where they need to track text size.
See references/a11y-patterns.md for Dynamic Type and adaptive layout examples, including @ScaledMetric and minimum tap target patterns.
Custom Rotors
Rotors let VoiceOver users quickly navigate to specific content types. Add custom rotors for content-heavy screens. See references/a11y-patterns.md for complete rotor examples.
System Accessibility Preferences
Always respect these environment values:
@Environment(\.accessibilityReduceMotion) var reduceMotion
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
@Environment(\.colorSchemeContrast) var contrast // .standard or .increased
@Environment(\.legibilityWeight) var legibilityWeight // .regular or .boldReduce Motion
Replace movement-based animations with crossfades or no animation:
withAnimation(reduceMotion ? nil : .spring()) {
showContent.toggle()
}
content.transition(reduceMotion ? .opacity : .slide)Review every moving transition, including row deletion, quantity changes, sheet or checkout presentation, and modal dismissal. Under Reduce Motion, replace slide, bounce, parallax, spring, and large spatial transitions with opacity changes, instant state changes, or no animation.
Reduce Transparency, Increase Contrast, Bold Text
// Solid backgrounds when transparency is reduced
.background(reduceTransparency ? Color(.systemBackground) : Color(.systemBackground).opacity(0.85))
// Stronger colors when contrast is increased
.foregroundStyle(contrast == .increased ? .primary : .secondary)
// Bold weight when system bold text is enabled
.fontWeight(legibilityWeight == .bold ? .bold : .regular)Decorative Content
// Decorative images: hidden from VoiceOver
Image(decorative: "background-pattern")
Image("visual-divider").accessibilityHidden(true)
// Icon next to text: Label handles this automatically
Label("Settings", systemImage: "gear")
// Icon-only buttons: MUST have an accessibility label
Button(action: { }) {
Image(systemName: "gear")
}
.accessibilityLabel("Settings")More from dpearson2699
Use this skill in a project
Open any RiverX project, search the Skills panel for dpearson2699/swift-ios-skills/ios-accessibility, and install it in one click.
