ios-accessibility
Skill13 starsChanged 7 months agoMITView on GitHub
Production-grade iOS accessibility skill covering VoiceOver, Dynamic Type, color contrast, motion, Switch Control, Voice Control, and WCAG 2.2 compliance for both SwiftUI and UIKit. This skill should be used when creating new iOS screens or views, reviewing existing iOS code for accessibility, adding VoiceOver support, fixing Dynamic Type issues, auditing WCAG compliance, implementing accessibilityLabel/traits/hints, grouping elements for assistive technology, adding custom actions or rotors, respecting system accessibility preferences (reduce motion, increase contrast, differentiate without color), writing accessibility-focused XCTest audits, or preparing apps for enterprise compliance (ADA, EAA, Section 508). Use this skill any time someone is working with iOS accessibility, VoiceOver, Dynamic Type, assistive technology, WCAG mapping, or accessibility modifiers in SwiftUI or UIKit — even if they only say 'make this accessible' or 'add VoiceOver support' or 'check contrast.' Also run an accessibility pass on any newly generated SwiftUI or UIKit view code before finalizing — AI coding assistants systematically produce inaccessible code by default (hardcoded fonts, onTapGesture instead of Button, missing labels, no system preference checks) and this skill corrects those patterns.
6 other projects use a version of this.
Adopted by
Most recently taken up by
- VelimirMueller/claude-skillstheir own version, first seen Nov 1, 2026
- VelimirMueller/ai-development-skillstheir own version, first seen Nov 1, 2026
- The-Artificer-of-Ciphers-LLC/skills-from-the-artificertheir own version, first seen Nov 1, 2026
- thienqo/ios-skillstheir own version, first seen Oct 11, 2026
- apexbymanish/claude-ai-agents-iostheir own version, first seen Oct 9, 2026
- dadederk/iOS-Accessibility-Agent-Skilltheir own version, first seen Mar 9, 2026
Other versions6 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 6
- dadederk/iOS-Accessibility-Agent-Skill★ 167same name
- VelimirMueller/ai-development-skills★ 7same name
- VelimirMueller/claude-skills★ 7same name
- The-Artificer-of-Ciphers-LLC/skills-from-the-artificer★ 6same name
- thienqo/ios-skills★ 4same name
- apexbymanish/claude-ai-agents-ios★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository11 other files it gives its agents.
- AGENTS.mdAGENTS.md
- gcd-operationsSkill
- ios-localizationSkill
- ios-loggingSkill
- ios-securitySkill
- ios-testingSkill
- swift-concurrencySkill
- swiftui-mvvmSkill
- tca-swiftuiSkill
- uikit-mvvmSkill
- viper-uikitSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
47 rules. 0 are common, 13 are distinctive, and 34 are very specific to this project.
0
Common
Most of the 6 edited versions we read keep these.
13
Distinctive
This author's own choices. Worth a look.
34
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 110
Read references/ai-failure-patterns.md — scan for all 11 AI failure patterns
- Distinctive: line 112
Search for 🔴 CRITICAL issues first (onTapGesture, missing labels, trait assignment)
- Distinctive: line 113
Search for 🟡 HIGH issues (hardcoded fonts, hardcoded colors, deprecated API)
- Very specific: line 114
Check system preference handling → read references/motion-input.md
- Very specific: line 115
If scope includes WCAG mapping → read references/wcag-ios-mapping.md
- Very specific: line 123
Read references/ai-failure-patterns.md — internalize patterns to avoid
- Very specific: line 124
Use Button for all interactive elements (never onTapGesture)
- Very specific: line 125
Use Dynamic Type text styles exclusively (never .system(size:))
- Very specific: line 126
Use semantic colors (.primary, .secondary, Color(.systemBackground))
- Very specific: line 127
Add .accessibilityLabel to every image button and custom control
- Very specific: line 128
Add .accessibilityAddTraits(.isHeader) to section headings
- Distinctive: line 130
Use custom actions for list cells with multiple buttons
- Very specific: line 131
Check @Environment(\.accessibilityReduceMotion) for any animations
- Very specific: line 132
Add .accessibilityValue to stateful controls (toggles, sliders, custom)
- Distinctive: line 133
Run confidence checks (below) before finalizing
- Distinctive: line 140
Read the relevant reference file for the fix pattern
- Distinctive: line 143
If the fix involves grouping or navigation, explain the user experience impact
- Distinctive: line 149
Run all 🔴 CRITICAL pattern checks — any finding blocks release
- Distinctive: line 150
Run 🟡 HIGH pattern checks — each needs risk acceptance or fix
- Very specific: line 151
Read references/testing.md — verify XCTest accessibility audit is configured
- Very specific: line 152
Read references/wcag-ios-mapping.md — check WCAG 2.2 AA coverage
- Very specific: line 160
Read references/dynamic-type.md — full Dynamic Type patterns
- Very specific: line 161
Search for .font(.system(size: — replace with text styles
- Very specific: line 162
Search for UIKit labels missing adjustsFontForContentSizeCategory = true
- Distinctive: line 163
Check layout adaptation: HStack→VStack at accessibility sizes
- Very specific: line 164
Verify ScrollView wraps content that may overflow
- Very specific: line 165
Check @ScaledMetric usage for non-text dimensions (icons, spacing)
- Distinctive: line 169
Use this table when asked which WCAG criteria apply, what's auto-handled, and what needs manual work.
- Distinctive: line 202
When asked "which WCAG criteria apply to iOS", always explicitly name 1.1.1 Non-text Content as manual (it's the most-missed), identify at …
- Very specific: line 217
Use .accessibilityLabeledPair(role:id:in:) (iOS 17+) to tell VoiceOver that a separate label view describes a nearby control.
- Very specific: line 231
When a single UIView draws its own hit-testable sub-regions (like bars in a bar chart), expose each region as a separate …
- Very specific: line 286
NEVER use onTapGesture for interactive elements — always use Button.
- Very specific: line 287
NEVER use .font(.system(size: N)) — always use Dynamic Type text styles (.title, .body, .caption, etc.).
- Very specific: line 288
NEVER use .foregroundColor(.black) or .background(Color.white) — use .foregroundStyle(.primary) and Color(.systemBackground).
- Very specific: line 289
ALWAYS add .accessibilityLabel to image-only buttons.
- Very specific: line 290
ALWAYS use .accessibilityAddTraits(.isHeader) on section headings — enables VoiceOver rotor heading navigation.
- Very specific: line 291
ALWAYS use .accessibilityElement(children: .combine) or .ignore + custom label to group related content (product cards, list cells, table …
- Very specific: line 293
NEVER hide disabled controls — use .disabled(true) (SwiftUI auto-adds .notEnabled trait) or .insert(.notEnabled) in UIKit.
- Very specific: line 294
Use modern SwiftUI API: .foregroundStyle() not .foregroundColor(), .clipShape(.rect(cornerRadius:)) not .cornerRadius(), NavigationStack …
- Very specific: line 296
Don't include the element type in accessibilityLabel — say "Play" not "Play button" (VoiceOver adds "button" from the trait).
- Very specific: line 297
Use Image(decorative:) for decorative images — not Image("bg").accessibilityHidden(true).
- Very specific: line 298
Use .accessibilityAddTraits(.isToggle) (iOS 17+) on any custom toggle-like control — VoiceOver announces "Toggle" so users know the control …
- Very specific: line 300
Check ALL system accessibility preferences — not just reduce motion.
- Distinctive: line 315
Ask the user. Rule of thumb: if removing the image changes the meaning of the screen, it's informative and needs a label.
- Very specific: line 318
Use .accessibilityRepresentation to provide an alternative accessible view (e.g., a custom gauge represented as a Slider for VoiceOver).
- Very specific: line 327
Use ViewThatFits (iOS 16+) to automatically switch between horizontal and vertical layouts.
- Distinctive: line 335
Before finalizing generated or reviewed code, verify ALL:
“Common” is measured: kept by at least 6 in 10 of the 6 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
iOS Accessibility
Production-grade accessibility skill for iOS codebases aligned with WCAG 2.2 AA and Apple Human Interface Guidelines. Operates correction-first — AI coding assistants (including Claude) systematically generate inaccessible iOS code because most training data lacks accessibility. This skill intercepts those patterns and enforces accessible output from the start.
The skill covers both SwiftUI and UIKit with framework-appropriate patterns, and produces code that works across VoiceOver, Switch Control, Voice Control, Full Keyboard Access, and Dynamic Type.
Why This Skill Exists
Research (CodeA11y, arXiv 2502.10884) found that AI omits accessibility because: (1) developers don't prompt for it so AI doesn't provide it, (2) most training code is inaccessible, and (3) generated code omits manual steps like replacing placeholder labels. This skill ensures every code generation pass includes accessibility from the start — not as an afterthought.
Quick Decision Trees
Which framework's patterns apply?
Is the file SwiftUI (.swift with View conformance)?
├── YES → Apply SwiftUI patterns → Read references/swiftui-patterns.md
└── NO → Is the file UIKit (.swift with UIView/UIViewController)?
├── YES → Apply UIKit patterns → Read references/uikit-patterns.md
└── NO → Mixed — apply BOTH checklists per file typeIs this element interactive?
Can the user tap/activate this element?
├── YES → Is it using Button, Toggle, Slider, Picker, or Link?
│ ├── YES → Standard control — verify accessibilityLabel exists
│ └── NO → Using onTapGesture or custom view?
│ └── 🔴 REPLACE with Button or appropriate standard control
│ onTapGesture is invisible to VoiceOver, Switch Control, and eye tracking
└── NO → Is it a meaningful image or icon?
├── YES → Add accessibilityLabel describing content, not appearance
└── NO → Decorative → Image(decorative:) or .accessibilityHidden(true)Does this text support Dynamic Type?
Is the font set with .font(.system(size: N))?
├── YES → 🔴 REPLACE with Dynamic Type style (.title, .body, .caption, etc.)
└── NO → Is it a custom font?
├── YES → SwiftUI: .custom("Name", size: N, relativeTo: .body)
│ UIKit: UIFontMetrics(forTextStyle:).scaledFont(for:)
└── NO → Using text style (.body, .headline, etc.)? → OKWhat grouping strategy for this container?
Is the container a single conceptual item? (e.g., product card)
├── YES → Are children's labels sufficient when joined?
│ ├── YES → .accessibilityElement(children: .combine)
│ └── NO → .accessibilityElement(children: .ignore) + custom label
└── NO → Are children independently interactive?
├── YES → .accessibilityElement(children: .contain) or default
└── NO → .combine or .ignore based on reading qualityWhat severity level applies?
Does the issue make content invisible to assistive technology?
├── YES → 🔴 CRITICAL (onTapGesture, missing labels on buttons, hidden disabled controls)
└── NO → Does it break under system accessibility settings?
├── YES → 🟡 HIGH (hardcoded fonts, hardcoded colors, ignoring reduce motion)
└── NO → Is it a defense-in-depth gap?
├── YES → 🟢 MEDIUM (missing hints, no custom actions on list cells, no rotors)
└── NO → 🔵 LOW (best practice: input labels for Voice Control, custom content)Severity Definitions
- 🔴 CRITICAL — Content or controls invisible to assistive technology.
onTapGesturefor interactive elements, missingaccessibilityLabelon icon buttons,isAccessibilityElement = falseon disabled controls, traits assigned instead of inserted (UIKit). - 🟡 HIGH — Breaks under accessibility settings. Hardcoded font sizes (
.system(size:)), hardcoded colors (.black,.white), deprecated API (foregroundColor,NavigationView,cornerRadius), ignoringreduceMotion, nonumberOfLines = 0on UIKit labels. - 🟢 MEDIUM — Degraded but not broken experience. Missing
accessibilityHint, no custom actions on list cells, missing.isHeadertraits on section headings, no grouping on related elements, noaccessibilityValueon stateful controls. - 🔵 LOW — Best practices. Missing
accessibilityInputLabelsfor Voice Control, noaccessibilityCustomContentfor secondary info, no Large Content Viewer on toolbar items.
Core AI Failure Patterns
For complete patterns with all 11 failure modes and code pairs, read
references/ai-failure-patterns.md
| # | AI Failure | Search for | Severity |
| F1 | onTapGesture instead of Button | .onTapGesture on interactive elements | 🔴 |
| F2 | Hardcoded font sizes | .font(.system(size: | 🟡 |
| F3 | Hardcoded colors for text/bg | .foregroundColor(.black), .background(Color.white) | 🟡 |
| F4 | Deprecated API | .foregroundColor(, .cornerRadius(, NavigationView | 🟡 |
| F5 | No labels on image buttons | Image(systemName: inside Button without .accessibilityLabel | 🔴 |
| F6 | GeometryReader abuse + fixed frames | GeometryReader, .frame(width: with hardcoded values | 🟡 |
| F7 | No accessibility on custom controls | Custom views without any accessibility modifiers | 🔴 |
| F8 | accessibilityIdentifier vs accessibilityLabel confusion | .accessibilityIdentifier used where .accessibilityLabel needed | 🔴 |
| F9 | Missing system preference checks | No @Environment for reduceMotion, dynamicTypeSize, etc. | 🟡 |
| F10 | Assigning traits instead of inserting (UIKit) | .accessibilityTraits = .selected (destroys existing traits) | 🔴 |
| F11 | Hiding disabled controls | isAccessibilityElement = false on disabled buttons | 🔴 |
Workflows
Workflow: Accessibility Review of Existing Code
When: User asks to "review accessibility", "check VoiceOver support", "audit a11y", or any variant.
- Read
references/ai-failure-patterns.md— scan for all 11 AI failure patterns - Determine framework: SwiftUI, UIKit, or mixed → read corresponding patterns reference
- Search for 🔴 CRITICAL issues first (onTapGesture, missing labels, trait assignment)
- Search for 🟡 HIGH issues (hardcoded fonts, hardcoded colors, deprecated API)
- Check system preference handling → read
references/motion-input.md - If scope includes WCAG mapping → read
references/wcag-ios-mapping.md - Report findings using the finding template (below)
- For regulated apps → read
references/compliance.md
Workflow: Generate Accessible New Screen
When: Creating a new SwiftUI or UIKit screen. Apply from the start so no retrofitting is needed.
- Read
references/ai-failure-patterns.md— internalize patterns to avoid - Use
Buttonfor all interactive elements (neveronTapGesture) - Use Dynamic Type text styles exclusively (never
.system(size:)) - Use semantic colors (
.primary,.secondary,Color(.systemBackground)) - Add
.accessibilityLabelto every image button and custom control - Add
.accessibilityAddTraits(.isHeader)to section headings - Group related elements with
.accessibilityElement(children: .combine) - Use custom actions for list cells with multiple buttons
- Check
@Environment(\.accessibilityReduceMotion)for any animations - Add
.accessibilityValueto stateful controls (toggles, sliders, custom) - Run confidence checks (below) before finalizing
Workflow: Fix Specific Accessibility Issue
When: User asks "how do I fix this VoiceOver issue" or "make this control accessible"
- Identify the current pattern and which AI failure it matches
- Read the relevant reference file for the fix pattern
- Provide the corrected code with explanation of WHY the fix works
- Include VoiceOver announcement text so the user can verify ("VoiceOver reads: ...")
- If the fix involves grouping or navigation, explain the user experience impact
Workflow: Accessibility Audit for Release
When: User asks "is this ready for release" or "accessibility checklist"
- Run all 🔴 CRITICAL pattern checks — any finding blocks release
- Run 🟡 HIGH pattern checks — each needs risk acceptance or fix
- Read
references/testing.md— verify XCTest accessibility audit is configured - Read
references/wcag-ios-mapping.md— check WCAG 2.2 AA coverage - For regulated apps → read
references/compliance.mdfor legal requirements - Output pass/fail gate with blocking issues listed
Workflow: Dynamic Type Compliance
When: User asks about Dynamic Type, text scaling, or font accessibility
- Read
references/dynamic-type.md— full Dynamic Type patterns - Search for
.font(.system(size:— replace with text styles - Search for UIKit labels missing
adjustsFontForContentSizeCategory = true - Check layout adaptation: HStack→VStack at accessibility sizes
- Verify
ScrollViewwraps content that may overflow - Check
@ScaledMetricusage for non-text dimensions (icons, spacing)
WCAG 2.2 AA — iOS Quick Reference
Use this table when asked which WCAG criteria apply, what's auto-handled, and what needs manual work. Native iOS platform behavior auto-satisfies many criteria, and some criteria simply do not apply to native iOS (they exist for HTML/web).
Criteria native iOS auto-handles
| Criterion | Why auto |
| 1.3.4 Orientation | iOS rotates automatically unless the app locks it |
| 1.4.4 Resize Text | Dynamic Type handles this when the app uses text styles |
| 2.1.1 Keyboard | Full Keyboard Access navigates standard controls |
| 2.4.7 Focus Visible | UIKit/SwiftUI show a focus ring in Full Keyboard Access |
| 1.4.10 Reflow | Auto Layout + Dynamic Type reflow by default |
Criteria that require MANUAL work
| Criterion | Manual action |
| 1.1.1 Non-text Content | Every meaningful image/icon needs accessibilityLabel; decorative images need Image(decorative:) or .accessibilityHidden(true) |
| 1.3.1 Info and Relationships | Heading traits (.isHeader), grouping (.accessibilityElement(children:)), table/form structure |
| 1.4.3 Contrast (AA) | 4.5:1 for body text, 3:1 for large text — test in BOTH light and dark modes independently |
| 1.4.6 Contrast Enhanced (AAA) | 7:1 body text — required when user enables Increase Contrast system setting |
| 2.5.7 Dragging Movements (WCAG 2.2 NEW) | Provide single-tap alternative via accessibilityCustomActions for any drag operation |
| 2.5.8 Target Size Minimum (WCAG 2.2 NEW) | 24×24pt minimum (AA), 44×44pt preferred (Apple HIG) |
| 3.3.7 Redundant Entry (WCAG 2.2 NEW) | Auto-fill or remember previously-entered info in multi-step forms |
| 3.3.8 Accessible Authentication (WCAG 2.2 NEW) | No cognitive puzzles for login; support paste, password managers, biometrics |
Criteria that DO NOT apply to native iOS
These are web/HTML-specific — calling them out is important because AI often copies them from web checklists:
- 4.1.1 Parsing — markup validity. iOS is not HTML; no parsing to validate. (Removed in WCAG 2.2.)
- 2.4.1 Bypass Blocks — "skip to main content". iOS uses the VoiceOver rotor instead.
- 4.1.2 Name, Role, Value — handled structurally by
accessibilityLabel/traits/valuein native code, not markup.
When asked "which WCAG criteria apply to iOS", always explicitly name 1.1.1 Non-text Content as manual (it's the most-missed), identify at least one of the four NEW 2.2 criteria (2.5.7, 2.5.8, 3.3.7, 3.3.8), and note that 4.1.1 and 2.4.1 don't apply.
Specific APIs Quick Reference
These APIs frequently come up in reviews but are easy to forget. Surfaced here from the references so the body of a response can cite them directly.
Form field label association (SwiftUI)
Text("Email:")
.accessibilityLabeledPair(role: .label, id: "emailField", in: namespace)
TextField("", text: $email)
.accessibilityLabeledPair(role: .content, id: "emailField", in: namespace)Use .accessibilityLabeledPair(role:id:in:) (iOS 17+) to tell VoiceOver that a separate label view describes a nearby control. Without it, VoiceOver reads them as independent elements and the user doesn't know the label belongs to the field.
Modal focus trapping (UIKit)
modalView.accessibilityViewIsModal = trueTwo requirements to make this work:
- Direct-child placement.
accessibilityViewIsModalonly hides the siblings of the modal view — not all other views in the hierarchy. The modal must be a direct child of the container whose siblings you want hidden (typically the window or a top-level container). - Escape gesture. Implement
accessibilityPerformEscapeon the modal so the VoiceOver user can dismiss it with a two-finger Z-gesture:override func accessibilityPerformEscape() -> Bool { dismiss(...); return true }
Custom-drawn UIKit elements (canvas, graphs, charts)
When a single UIView draws its own hit-testable sub-regions (like bars in a bar chart), expose each region as a separate UIAccessibilityElement:
override var accessibilityElements: [Any]? {
bars.map { bar in
let element = UIAccessibilityElement(accessibilityContainer: self)
element.accessibilityLabel = "\(bar.label), \(bar.value)"
element.accessibilityTraits = .staticText
// Use SCREEN coordinates for custom-drawn elements:
element.accessibilityFrame = UIAccessibility.convertToScreenCoordinates(bar.frame, in: self)
return element
}
}Set isAccessibilityElement = false on the parent container so VoiceOver sees the children. Use UIAccessibility.convertToScreenCoordinates(_:in:) for the frame — this is the canonical API for custom-drawn views because the system expects screen-space coordinates for hit-testing the VoiceOver cursor. (accessibilityFrameInContainerSpace is an alternative but requires the view to be in a standard container hierarchy, which custom-drawn graphs often aren't.)
UIKit row/cell grouping order
cell.shouldGroupAccessibilityChildren = true // group children into one swipe stop
titleLabel.accessibilitySortPriority = 2 // read first (higher = earlier)
subtitleLabel.accessibilitySortPriority = 1 // read secondshouldGroupAccessibilityChildren keeps VoiceOver focused on the cell (prevents wandering into children) while accessibilitySortPriority controls reading order within the group.
Screen Curtain test
The gold-standard VoiceOver manual test: enable VoiceOver, then triple-tap with three fingers to toggle Screen Curtain — the display goes black while VoiceOver continues. Navigate the screen this way. If you can't complete the task, there's an accessibility gap. Every release checklist should include a Screen Curtain pass for critical flows.
Finding Report Template
### [SEVERITY] [Short title]
**File:** `path/to/file.swift:42`
**WCAG:** [Criterion if applicable] | **HIG:** [Guideline if applicable]
**Issue:** [1-2 sentence description]
**VoiceOver Impact:** [What the user hears/doesn't hear]
**Fix:**
```swift
// ❌ Current
[problematic code]
// ✅ Corrected
[accessible replacement]
<critical_rules>
## Code Generation Rules
Whether generating new code or reviewing existing code, ALWAYS enforce these rules. They correct the systematic accessibility failures that AI coding assistants produce:
1. NEVER use `onTapGesture` for interactive elements — always use `Button`. Views with `onTapGesture` are invisible to VoiceOver, Switch Control, and visionOS eye tracking. This is the #1 AI failure.
2. NEVER use `.font(.system(size: N))` — always use Dynamic Type text styles (`.title`, `.body`, `.caption`, etc.). Hardcoded sizes break for 25%+ of users who change text size.
3. NEVER use `.foregroundColor(.black)` or `.background(Color.white)` — use `.foregroundStyle(.primary)` and `Color(.systemBackground)`. Hardcoded colors are invisible in dark mode.
4. ALWAYS add `.accessibilityLabel` to image-only buttons. `Image(systemName: "plus")` inside a `Button` reads as the raw SF Symbol name without a label.
5. ALWAYS use `.accessibilityAddTraits(.isHeader)` on section headings — enables VoiceOver rotor heading navigation.
6. ALWAYS use `.accessibilityElement(children: .combine)` or `.ignore` + custom label to group related content (product cards, list cells, table rows).
7. In UIKit, ALWAYS use `.insert(.selected)` — never `.accessibilityTraits = .selected` which DESTROYS existing traits like `.isButton`.
8. NEVER hide disabled controls — use `.disabled(true)` (SwiftUI auto-adds `.notEnabled` trait) or `.insert(.notEnabled)` in UIKit. VoiceOver reads "dimmed" so the user knows the control exists.
9. Use modern SwiftUI API: `.foregroundStyle()` not `.foregroundColor()`, `.clipShape(.rect(cornerRadius:))` not `.cornerRadius()`, `NavigationStack` not `NavigationView`.
10. For ANY animation, check `@Environment(\.accessibilityReduceMotion)` — replace motion with crossfade/opacity when enabled.
11. Don't include the element type in `accessibilityLabel` — say "Play" not "Play button" (VoiceOver adds "button" from the trait).
12. Use `Image(decorative:)` for decorative images — not `Image("bg").accessibilityHidden(true)`. When images inside a grouped container are purely visual (e.g., star icons in a rating display), hide each individual image with `.accessibilityHidden(true)` and provide a single meaningful description on the parent.
13. Use `.accessibilityAddTraits(.isToggle)` (iOS 17+) on any custom toggle-like control — VoiceOver announces "Toggle" so users know the control switches between states. Group the toggle's label and visual indicator with `.accessibilityElement(children: .combine)` or `.ignore`.
14. Use `@ScaledMetric(relativeTo:)` for non-text dimensions — including **image sizes**, icons, spacing, and avatars — so they scale proportionally with Dynamic Type: `@ScaledMetric(relativeTo: .body) var iconSize: CGFloat = 24`. Wrap content in `ScrollView` for overflow at accessibility text sizes. For UIKit labels always set `numberOfLines = 0` so scaled text can wrap. Toolbar/tab bar items should **cap** at `.xxxLarge` via `.dynamicTypeSize(...DynamicTypeSize.xxxLarge)` — unbounded scaling breaks toolbar layouts, and iOS shows a Large Content Viewer for bigger sizes instead.
15. Check ALL system accessibility preferences — not just reduce motion. Each has an `@Environment` key AND, where applicable, a SwiftUI/UIKit opt-out modifier:
- `reduceMotion` — replace motion with crossfade/opacity
- `reduceTransparency` — use opaque backgrounds instead of `.ultraThinMaterial`
- `legibilityWeight` (Bold Text) — honor via `Font.weight(legibilityWeight == .bold ? .bold : .regular)`
- `colorSchemeContrast` — when `.increased`, contrast ratio targets rise from **4.5:1 (AA) to 7:1 (AAA)** for body text; test contrast in BOTH light and dark modes independently
- `accessibilityDifferentiateWithoutColor` — never convey status with color alone; add a shape, icon, or pattern
- `invertColors` — photos, videos, and correctly-colored icons should opt out via `.accessibilityIgnoresInvertColors(true)` (SwiftUI) or `accessibilityIgnoresInvertColors = true` (UIKit). This property **does not cascade** to child views — apply it on each leaf view that renders photo content.
- `dynamicTypeSize` — use `@Environment(\.dynamicTypeSize)` to switch HStack → VStack at `.accessibility1` and above
16. For WCAG 2.5.7 (Dragging Movements, new in WCAG 2.2) and 2.5.8 (Target Size Minimum): provide single-tap alternatives for drag operations using `accessibilityCustomActions`, and ensure touch target size is at least **24×24pt** (WCAG 2.5.8 AA minimum) or ideally **44×44pt** (Apple HIG). When answering drag-and-drop accessibility questions, cite **WCAG 2.5.7** specifically — it is the criterion that directly applies.
</critical_rules>
<fallback_strategies>
## Fallback Strategies & Loop Breakers
**If unsure whether an image is decorative or informative:**
Ask the user. Rule of thumb: if removing the image changes the meaning of the screen, it's informative and needs a label. If it's purely aesthetic, it's decorative.
**If a custom control is too complex for standard accessibility modifiers:**
Use `.accessibilityRepresentation` to provide an alternative accessible view (e.g., a custom gauge represented as a Slider for VoiceOver).
**If VoiceOver reading order is wrong after layout changes:**
In SwiftUI, use `.accessibilitySortPriority()` (higher = read earlier). In UIKit, override `accessibilityElements` array to define explicit order.
**If modal focus trapping isn't working:**
In UIKit, verify `accessibilityViewIsModal = true` is on the modal AND the modal is a **direct child** of the container whose siblings you want hidden — this property only hides SIBLINGS, not all other views. Also implement `accessibilityPerformEscape` on the modal so VoiceOver users can dismiss it with the two-finger Z-gesture. See "Specific APIs Quick Reference → Modal focus trapping" above.
**If Dynamic Type breaks layout at accessibility sizes:**
Use `ViewThatFits` (iOS 16+) to automatically switch between horizontal and vertical layouts. Wrap content in `ScrollView` for overflow. Use `AnyLayout` to preserve state during layout changes.
**If grouping with `.combine` produces awkward VoiceOver reading:**
Switch to `.ignore` + custom `accessibilityLabel` with a natural sentence. `.combine` joins labels with pauses; `.ignore` lets you write a coherent sentence.
</fallback_strategies>
## Confidence Checks
Before finalizing generated or reviewed code, verify ALL:
[ ] No onTapGesture on interactive elements — all use Button or standard controls [ ] No hardcoded font sizes — all text uses Dynamic Type styles [ ] No hardcoded colors for text/backgrounds — all use semantic colors [ ] Every image-only button has accessibilityLabel [ ] Section headings have .accessibilityAddTraits(.isHeader) [ ] Related content grouped with .accessibilityElement(children:) [ ] List cells with multiple actions use accessibilityCustomActions [ ] Animations check @Environment(.accessibilityReduceMotion) [ ] Disabled controls visible to VoiceOver (not hidden, using .notEnabled) [ ] UIKit traits inserted (.insert) not assigned (= .trait) [ ] Labels describe purpose, not appearance ("Add to favorites" not "Heart icon") [ ] Labels don't include element type ("Play" not "Play button") [ ] Decorative images hidden from VoiceOver [ ] Custom controls have label, value, traits, and adjustable action where appropriate [ ] Custom toggles use .accessibilityAddTraits(.isToggle) and .accessibilityValue("On"/"Off") [ ] Non-text dimensions (icons, spacing) use @ScaledMetric for Dynamic Type scaling [ ] Content wrapped in ScrollView for overflow at accessibility text sizes [ ] All system preferences checked: reduceMotion, reduceTransparency, legibilityWeight, colorSchemeContrast, differentiateWithoutColor, invertColors [ ] Touch targets meet 44×44pt minimum (Apple HIG) / 24×24pt (WCAG 2.5.8) [ ] Drag operations have single-tap alternatives via accessibilityCustomActions [ ] Modern API used (foregroundStyle, clipShape, NavigationStack)
## Companion Skills
| Finding type | Companion skill | Apply when |
|---|---|---|
| Architecture patterns affecting accessibility | `skills/swiftui-mvvm/SKILL.md` | Structuring ViewModels that manage accessibility state |
| Security + accessibility overlap (biometric auth) | `skills/ios-security/SKILL.md` | LAContext with proper VoiceOver feedback |
| Testing accessibility in CI | `skills/ios-testing/SKILL.md` | XCTest accessibility audits, snapshot tests |
| Concurrency in VoiceOver announcements | `skills/swift-concurrency/SKILL.md` | Posting notifications from async contexts |
## References
| Reference | When to Read |
|-----------|-------------|
| `references/rules.md` | Do's and Don'ts quick reference: priority rules and critical anti-patterns |
| `references/ai-failure-patterns.md` | Every code generation/review — all 11 AI failure patterns with ❌/✅ code pairs |
| `references/voiceover-patterns.md` | VoiceOver work — labels, hints, traits, grouping, custom actions, rotors, navigation |
| `references/swiftui-patterns.md` | SwiftUI accessibility — all modifiers, component patterns, focus management |
| `references/uikit-patterns.md` | UIKit accessibility — elements, containers, traits, notifications, modal views |
| `references/dynamic-type.md` | Dynamic Type — UIFontMetrics, @ScaledMetric, layout adaptation, Large Content Viewer |
| `references/color-visual.md` | Color/contrast — WCAG ratios, color blindness, dark mode, reduce transparency, smart invert |
| `references/motion-input.md` | Motion & input — reduce motion, switch control, voice control, full keyboard access |
| `references/wcag-ios-mapping.md` | WCAG compliance — 2.2 AA criteria mapped to iOS APIs, what's auto-handled vs manual |
| `references/testing.md` | Testing — Xcode Accessibility Inspector, XCTest audits, VoiceOver manual protocol, CI setup |
| `references/ios-new-features.md` | iOS 17/18/26 — new accessibility APIs, modifiers, and platform features |
| `references/compliance.md` | Regulated apps — ADA, EAA, Section 508, VPAT, documentation requirements |Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

