From swift-accessibility-skill
Applies platform accessibility best practices to SwiftUI, UIKit, and AppKit code, covering VoiceOver, Dynamic Type, Reduce Motion, and all Nutrition Label categories. Ensures a11y from first draft in UI writing, editing, or review.
npx claudepluginhub pasqualevittoriosi/swift-accessibility-skill --plugin swift-accessibility-skillThis skill uses the workspace's default tool permissions.
Apply accessibility for SwiftUI, UIKit, and AppKit across all supported platforms. Covers all 9 App Store Accessibility Nutrition Label categories — VoiceOver, Voice Control, Larger Text, Dark Interface, Differentiate Without Color, Sufficient Contrast, Reduced Motion, Captions, and Audio Descriptions.
examples/before-after-appkit.mdexamples/before-after-swiftui.mdexamples/before-after-uikit.mdreferences/assistive-access.mdreferences/display-settings.mdreferences/dynamic-type.mdreferences/media-accessibility.mdreferences/motor-input.mdreferences/nutrition-labels.mdreferences/platform-specifics.mdreferences/semantic-structure.mdreferences/testing-auditing.mdreferences/voice-control.mdreferences/voiceover-swiftui.mdreferences/voiceover-uikit.mdreferences/wcag-mapping.mdresources/audit-template.swiftresources/qa-checklist.mdDiagnoses and fixes accessibility issues in iOS/macOS apps: VoiceOver labels, Dynamic Type, color contrast, touch targets, keyboard navigation, Reduce Motion for WCAG/App Store compliance.
Implements, reviews, or improves accessibility in iOS/macOS apps with SwiftUI and UIKit, covering VoiceOver, focus management, Dynamic Type, custom rotors, and XCTest testing.
Provides best practices and SwiftUI code examples for iOS accessibility, including VoiceOver labels, Dynamic Type support, touch targets, and HIG compliance.
Share bugs, ideas, or general feedback.
Apply accessibility for SwiftUI, UIKit, and AppKit across all supported platforms. Covers all 9 App Store Accessibility Nutrition Label categories — VoiceOver, Voice Control, Larger Text, Dark Interface, Differentiate Without Color, Sufficient Contrast, Reduced Motion, Captions, and Audio Descriptions.
This skill prioritizes native platform APIs (which provide free automatic support) and fact-based guidance without architecture opinions.
Include accessibility in the first draft — never write a bare element and patch it later. Retrofitting accessibility is harder, gets skipped, and produces worse results than building it in from the start.
No inline commentary unless a pattern is non-obvious. Mark inferred labels with // [VERIFY] because SF Symbol names don't always match the intended user-facing meaning.
| Situation | Required on first write |
|---|---|
Button / NavigationLink — icon-only | .accessibilityLabel("…") with // [VERIFY] |
Button / NavigationLink — visible text | Nothing extra — text is the label automatically |
Image — meaningful | .accessibilityLabel("…") |
Image — decorative | .accessibilityHidden(true) |
withAnimation / .transition / .animation | @Environment(\.accessibilityReduceMotion) + gate animation |
.font(.system(size:)) | Replace with .font(.body) or @ScaledMetric |
| Color conveys state/status | Add shape, icon, or text alongside color |
onTapGesture on non-Button | .accessibilityElement(children: .ignore) + .accessibilityAddTraits(.isButton) + .accessibilityLabel |
| Custom slider / toggle / stepper | .accessibilityRepresentation { … } or .accessibilityValue + .accessibilityAdjustableAction |
| Async content change | Post announcement with availability guards (AccessibilityNotification.Announcement on iOS 17+, fallback to UIAccessibility.post) |
System .sheet / .fullScreenCover | Nothing extra — SwiftUI traps focus automatically (custom overlays still need focus management) |
AVPlayer / video | Use AVPlayerViewController — captions and Audio Descriptions for free |
| Custom tappable view | .frame(minWidth: 44, minHeight: 44) |
| Any new SwiftUI view | Verify with Xcode Canvas Variants (see Accessibility Summary) |
NSButton — icon-only (AppKit) | setAccessibilityLabel("…") with // [VERIFY] |
Custom NSView interactive element (AppKit) | setAccessibilityElement(true) + role (setAccessibilityRole(.button)) + label |
| AppKit modal/popup UI | Trap focus and ensure dismiss action is keyboard + VoiceOver reachable |
| Any new AppKit view/controller | Verify with Accessibility Inspector and full keyboard navigation |
Prefer native controls (Button, Toggle, Stepper, Slider, Picker, TextField) — they get full accessibility automatically. Custom interactive views require explicit work.
For AppKit, prefer native controls (NSButton, NSPopUpButton, NSSlider, NSSegmentedControl, NSTextField) before custom NSView interaction.
Example — icon-only button:
Button {
shareAction()
} label: {
Image(systemName: "square.and.arrow.up")
}
.accessibilityLabel("Share") // [VERIFY] confirm label matches intent
Example — gating animation on Reduce Motion:
@Environment(\.accessibilityReduceMotion) private var reduceMotion
withAnimation(reduceMotion ? nil : .spring()) {
isExpanded.toggle()
}
Full testing and verification procedures → references/testing-auditing.md
Before answering, select one primary reference file that best matches the user's intent and load it first. Load additional reference files only when the request explicitly spans multiple domains (for example, VoiceOver + Dynamic Type + WCAG mapping) or when the primary file does not cover a required criterion.
Apply First-Draft Rules — accessibility in the first draft, no commentary.
For APIs introduced after iOS 15, always add #available guards and provide older-OS fallback behavior.
After writing, verify against the First-Draft Rules table — fix any gaps before outputting.
After the code, append an Accessibility Summary (see below).
Apply fixes silently, no commentary.
For APIs introduced after iOS 15, always add #available guards and provide older-OS fallback behavior.
After fixing, verify against the First-Draft Rules table — fix any gaps before outputting.
After the code, append an Accessibility Summary.
examples/before-after-swiftui.md, examples/before-after-uikit.md, or examples/before-after-appkit.mdreferences/platform-specifics.mdOnly when user explicitly asks ("audit", "how accessible is this?", "review accessibility").
Quick fix mode — when the user asks for blocker-only/critical-only scope (for example: "just fix the blockers", "quick fix", "critical only"): address only Blocks Assistive Tech and Degrades Experience issues. Skip Incomplete Support.
Comprehensive mode (default) — address all severity levels including Incomplete Support and Nutrition Label gaps.
references/wcag-mapping.mdresources/qa-checklist.md→ references/nutrition-labels.md — all 9 categories with official pass/fail criteria
When the user asks to prepare or draft an App Store Accessibility Nutrition Label recommendation, output this format:
**Accessibility Nutrition Label recommendation**
**App version evaluated:** [version or "Current build"]
**Scope reviewed:** [common tasks / screens evaluated]
**You could claim:**
- [labels where every common task is ✅ or —]
**Why you could claim them:**
- [label]: [brief reason tied to completed common-task coverage]
**You should not claim:**
- [labels blocked by any ❌]
- [labels that are not applicable]
**Why you should not claim them:**
- [label]: [blocked task or why the label is not applicable]
**Common-task verification**
| Common Task | VoiceOver | Voice Control | Larger Text | Dark Mode | No Color | Contrast | Motion | Captions | Audio Desc |
|---|---|---|---|---|---|---|---|---|---|
| [task] | ✅ / ❌ / — | ✅ / ❌ / — | ✅ / ❌ / — | ✅ / ❌ / — | ✅ / ❌ / — | ✅ / ❌ / — | ✅ / ❌ / — | ✅ / ❌ / — | ✅ / ❌ / — |
**Recommendation summary**
- You could claim: [labels]
- You should not claim: [labels]
Do not say "claim" without qualification. Phrase the output as a recommendation based on the reviewed scope.
Do not suggest a label if any common task in that column is ❌. Use — only when the label is genuinely not applicable to that app or flow.
Append after all code generation and fix tasks (modes 1, 2), unless the user explicitly requests code-only output. No preamble.
**Accessibility applied:**
- [one bullet per pattern added — e.g. "`.accessibilityLabel` on icon-only Share button"]
**Verify in Xcode:**
- Use Canvas **Dynamic Type Variants** (grid icon → Dynamic Type Variants) to check layout at all text sizes
- Use Canvas **Color Scheme Variants** to check light and dark mode
- Use **Accessibility Inspector** (Xcode → Open Developer Tool) Settings tab to simulate Increase Contrast, Reduce Motion, Bold Text on the Simulator
**If Xcode is unavailable:**
- Run equivalent checks with platform accessibility inspector tools and manual setting toggles (Dynamic Type, Contrast, Reduce Motion, VoiceOver/Voice Control)
**Test on device:**
- [relevant items from Must Test on Device checklist]
Omit "Accessibility applied" entirely if nothing was added (all native controls). Omit "Nutrition Label readiness" unless the user asked about it.
Only when user explicitly requests an audit. Never during code generation or fixes.
🔴 Blocks Assistive Tech — completely unreachable, fix immediately 🟡 Degrades Experience — reachable but significant friction 🟠 Incomplete Support — gaps preventing Nutrition Label claims ✅ Verified in code — confirmed correct by static analysis
Close with:
Must test on device: relevant items from the Review Checklist. Nutrition Label readiness: Achievable / Blocked by [issue] / Not applicable.
.accessibilityLabel to a Button with visible text actually hurts — it overrides the text VoiceOver would read automatically.#available. Version-specific APIs crash on older OS without availability checks.[VERIFY]. SF Symbol names (e.g. square.and.arrow.up) rarely match what users expect to hear ("Share"). Inferred labels need human review.UIAccessibility.isVoiceOverRunning. Adapt to the actual user need by checking the relevant accessibility setting directly. Narrow coordination exceptions are fine, such as avoiding overlapping speech or extending transient timeouts while assistive tech is active..accessibilityLabel — blank is never acceptable.accessibilityHidden(true).accessibilityAddTraits(.isSelected) not "Selected photo".accessibilityElement(children: .combine)AccessibilityNotification.Announcement("Upload complete").post(), fallback UIAccessibility.post(notification: .announcement, argument: "Upload complete")references/voiceover-swiftui.md or references/voiceover-uikit.md.accessibilityInputLabels(["Compose", "New Message"]) for icon-only elements.accessibilityAction)references/voice-control.md.font(.body) not .font(.system(size: 16))@ScaledMetric(relativeTo: .body) var spacing: CGFloat = 8.accessibilityShowsLargeContentViewer()ViewThatFits (iOS 16+) over manual dynamicTypeSize checks — it automatically picks the layout that fitsreferences/dynamic-type.mdColor(.label)); WCAG 4.5:1 text, 3:1 non-text.ultraThinMaterial with opaque when enabledreferences/display-settings.md.accessibilitySortPriority(_:) (higher = read first).screenChanged notificationaccessibilityViewIsModal = trueaccessibilityRotor(_:entries:)references/semantic-structure.mdUIAccessibilityCustomAction for swipe-only gesturesreferences/motor-input.md| Modifier | Purpose |
|---|---|
.accessibilityLabel(_:) | VoiceOver text for non-text elements |
.accessibilityHint(_:) | Brief result description |
.accessibilityValue(_:) | Current value (sliders, progress) |
.accessibilityHidden(true) | Hide decorative elements |
.accessibilityAddTraits(_:) | Semantic role or state |
.accessibilityRemoveTraits(_:) | Remove inherited trait |
.accessibilityElement(children:) | .combine / .contain / .ignore |
.accessibilitySortPriority(_:) | Reading order (higher = earlier) |
.accessibilityAction(_:_:) | Named custom action |
.accessibilityAdjustableAction(_:) | Increment/decrement |
.accessibilityInputLabels(_:) | Voice Control alternate names |
.accessibilityFocused(_:) | Programmatic focus |
.accessibilityRotor(_:entries:) | Custom VoiceOver rotor |
.accessibilityRepresentation(_:) | Replace AX tree for custom controls |
.accessibilityIgnoresInvertColors(true) | Protect images in Smart Invert |
.accessibilityShowsLargeContentViewer() | Large Content Viewer for fixed-size UI |
| Value | Purpose |
|---|---|
\.accessibilityReduceMotion | Gate animations |
\.accessibilityReduceTransparency | Replace blur effects |
\.accessibilityDifferentiateWithoutColor | Add non-color indicators |
\.colorSchemeContrast | .standard / .increased |
\.dynamicTypeSize | Current text size |
| Label | Key APIs | Reference |
|---|---|---|
| VoiceOver | accessibilityLabel, traits, actions, rotors | voiceover-swiftui.md, voiceover-uikit.md |
| Voice Control | accessibilityInputLabels, visible text match | voice-control.md |
| Larger Text | @ScaledMetric, text styles, Large Content Viewer | dynamic-type.md |
| Dark Interface | colorScheme, semantic colors | display-settings.md |
| Differentiate Without Color | shapes + color | display-settings.md |
| Sufficient Contrast | WCAG 4.5:1 text / 3:1 non-text | display-settings.md |
| Reduced Motion | accessibilityReduceMotion, animation gate | display-settings.md |
| Captions | AVPlayerViewController | media-accessibility.md |
| Audio Descriptions | AVMediaCharacteristic.describesVideoForAccessibility | media-accessibility.md |
.accessibilityLabel.accessibilityHidden(true)@ScaledMetricaccessibilityReduceMotion.accessibilityInputLabels on icon-only elements.sheet() or accessibilityViewIsModal.accessibilityAction alternativesAVPlayerViewController for video.accessibilityIgnoresInvertColors()performAccessibilityAudit() with #available guards (iOS 17+ / macOS 14+), plus fallback assertions on older OS versionsFull testing procedures → references/testing-auditing.md
references/voiceover-swiftui.mdreferences/voiceover-swiftui.mdreferences/voiceover-uikit.mdreferences/semantic-structure.mdreferences/voice-control.mdreferences/voice-control.mdreferences/motor-input.mdreferences/display-settings.mdreferences/semantic-structure.mdreferences/semantic-structure.mdreferences/dynamic-type.mdreferences/display-settings.mdreferences/display-settings.mdreferences/media-accessibility.mdreferences/platform-specifics.mdreferences/nutrition-labels.mdSymptom: SwiftUI modifiers used in UIKit/AppKit code, or platform APIs mixed across frameworks.
Fix: Identify framework from imports (import SwiftUI, import UIKit, import AppKit) before applying APIs. SwiftUI uses modifiers, UIKit uses UIAccessibility properties, and AppKit uses NSAccessibility APIs.
Symptom: .accessibilityLabel added to a Button("Save") or Toggle("Dark Mode") that already has visible text.
Fix: Do not add .accessibilityLabel when the control has visible text — it overrides the automatic label and can desync with what's on screen. Only add labels to icon-only or non-text elements.
Symptom: Code uses platform-versioned APIs (iOS/macOS/tvOS/watchOS/visionOS) without availability checks.
Fix: Gate with #available for every target OS you support and use older equivalents when needed. Common substitutions:
AccessibilityNotification.Announcement("…").post() → UIAccessibility.post(notification: .announcement, argument: "…")performAccessibilityAudit() → manual XCTest assertionsViewThatFits (iOS 16+) → @Environment(\.dynamicTypeSize) with manual layout switchingif #available(iOS 17, macOS 14, tvOS 17, watchOS 10, visionOS 1, *) { ... }Symptom: Code generated without the "Accessibility applied" / "Test on device" summary block. Fix: Always append the Accessibility Summary after code generation and fix tasks (workflow modes 1 and 2). Omit only when no accessibility patterns were added (all native controls with visible text).
Symptom: .accessibilityLabel derived from SF Symbol names or method names without a // [VERIFY] comment.
Fix: Any label that was inferred (not provided by the user) must include // [VERIFY] confirm label matches intent. SF Symbol names like square.and.arrow.up rarely match what users expect to hear.
references/voiceover-swiftui.md — SwiftUI accessibility modifiers, traits, actions, rotors, announcementsreferences/voiceover-uikit.md — UIAccessibility protocol, custom elements, containers, notificationsreferences/voice-control.md — Input labels, "Show numbers/names", voice-accessible alternativesreferences/motor-input.md — Switch Control, Full Keyboard Access, AssistiveTouch, tvOS focusreferences/dynamic-type.md — Dynamic Type, @ScaledMetric, Large Content Viewer, adaptive layoutsreferences/display-settings.md — Reduce Motion, Contrast, Dark Mode, Color, Transparency, Invertreferences/semantic-structure.md — Grouping, reading order, focus management, rotors, modal focusreferences/media-accessibility.md — Captions, Audio Descriptions, Speech synthesis, Chartsreferences/testing-auditing.md — Accessibility Inspector, Xcode Canvas Variants, XCTest, performAccessibilityAudit(), manual testingreferences/nutrition-labels.md — All 9 Nutrition Labels with pass/fail criteriareferences/wcag-mapping.md — WCAG 2.2 Level A/AA success criteria mapped to SwiftUI/UIKit/AppKit APIsreferences/assistive-access.md — Assistive Access (iOS 17+), design principles, testingreferences/platform-specifics.md — macOS, watchOS, tvOS, visionOS specificsexamples/before-after-swiftui.md — SwiftUI before/after transformationsexamples/before-after-uikit.md — UIKit before/after transformationsexamples/before-after-appkit.md — AppKit (macOS) before/after transformationsresources/audit-template.swift — Drop-in XCUITest file for automated accessibility auditing (iOS 17+)resources/qa-checklist.md — Standalone QA checklist for manual testing (hand to testers)