Claude
Skills
Sign in
Back

ios-accessibility

Included with Lifetime
$97 forever

Implements, reviews, or improves accessibility in iOS/macOS apps with SwiftUI, UIKit, and AppKit. Use when adding VoiceOver, Voice Control, Switch Control, or Full Keyboard Access support; when working with accessibility labels, hints, values, traits, accessibilityInputLabels, NSAccessibility, grouping, reading order, accessibility focus restoration with @AccessibilityFocusState, Dynamic Type, @ScaledMetric, custom rotors, accessibility actions, XCTest accessibility checks, App Store Accessibility Nutrition Labels, App Store Connect accessibility answers, a11y compliance audits, or system accessibility preferences.

Image & Video

What this skill does


# iOS/macOS Accessibility - SwiftUI, UIKit, and AppKit

Every user-facing view must be usable with VoiceOver, Switch Control, Voice Control, Full Keyboard Access, and other assistive technologies. This skill covers SwiftUI, UIKit, and AppKit patterns required to build accessible iOS, iPadOS, and macOS apps.

## Contents

- [Core Principles](#core-principles)
- [How VoiceOver Reads Elements](#how-voiceover-reads-elements)
- [SwiftUI Accessibility Modifiers](#swiftui-accessibility-modifiers)
- [Focus Management](#focus-management)
- [Dynamic Type](#dynamic-type)
- [Custom Rotors](#custom-rotors)
- [System Accessibility Preferences](#system-accessibility-preferences)
- [Decorative Content](#decorative-content)
- [Voice Control](#voice-control)
- [Switch Control](#switch-control)
- [Full Keyboard Access](#full-keyboard-access)
- [Assistive Access (iOS 18+)](#assistive-access-ios-18)
- [UIKit Accessibility Patterns](#uikit-accessibility-patterns)
- [AppKit Accessibility Patterns](#appkit-accessibility-patterns)
- [Accessibility Custom Content](#accessibility-custom-content)
- [App Store Accessibility Nutrition Labels](#app-store-accessibility-nutrition-labels)
- [Testing Accessibility](#testing-accessibility)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)

---

## Core Principles

1. Every interactive element MUST have an accessible label. If no visible text exists, add `.accessibilityLabel`.
2. Every custom control MUST have correct traits via `.accessibilityAddTraits` (never direct assignment). For binary custom controls such as favorite/star buttons, prefer a real `Toggle`; otherwise expose toggle behavior with `.accessibilityAddTraits(.isToggle)` and a current state value without putting the control type in the label.
3. Custom adjustable controls such as quantity steppers MUST expose adjustable behavior with `.accessibilityAdjustableAction`; UIKit custom adjustable controls also need the `.adjustable` trait.
4. Decorative images MUST be hidden from assistive technologies.
5. Sheet and dialog dismissals MUST return VoiceOver focus to the trigger element.
6. All tap targets MUST be at least 44x44 points.
7. Dynamic Type MUST be supported everywhere (system fonts, `@ScaledMetric`, adaptive layouts).
8. No information conveyed by color alone -- always provide text or icon alternatives.
9. System accessibility preferences MUST be respected: Reduce Motion, Reduce Transparency, Bold Text, Increase Contrast.

## 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](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.

When triaging broad focus bugs, still call out accessibility traversal separately: accessibility element order and grouping in the view hierarchy directly affect VoiceOver swipe order, Switch Control scan order, Voice Control overlay targeting, and Full Keyboard Access reachability review. Route keyboard-focus implementation to `focus-engine`, but keep this traversal impact in `ios-accessibility`.

### `@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.

```swift
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

```swift
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:

```swift
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:

```swift
// 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](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](references/a11y-patterns.md) for complete rotor examples.

## System Accessibility Preferences

Always respect these environment values:

```swift
@Environment(\.accessibilityReduceMotion) var reduceMotion
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
@Environment(\.colorSchemeContrast) var contrast         // .standard or .increased
@Environment(\.legibilityWeight) var legibilityWeight    // .regular or .bold
```

### Reduce Motion

Replace movement-based animations with crossfades or no animation:

```swift
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.

###

Related in Image & Video