Swiftui accessibility
Skill Tyr0/agent-skills/plugins/swiftui-expert/skills/swiftui-accessibility
A collection of skills, plugins, and agents for AI workflows.
npx -y skills add Tyr0/agent-skills --skill swiftui-accessibilityAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use this skill whenever the user asks about SwiftUI accessibility — VoiceOver, accessibility modifiers, accessibility labels/values/hints/traits, custom actions, accessibility rotors, Dynamic Type, Reduce Motion, increase contrast, differentiate without color, accessibility identifiers for UI testing, custom accessibility elements (`accessibilityElement(children:)`), or auditing a SwiftUI view for accessibility. Targets iOS 17+. Triggers on questions like "is this accessible", "how do I label this for VoiceOver", "Dynamic Type support", "how do I group children for accessibility", or any a11y review.
SKILL.md
10.0 KB, as published. Nobody here has run it
SwiftUI Accessibility
SwiftUI gives you a lot of accessibility for free — and a lot of ways to silently break it. This is the audit checklist plus the modifier vocabulary.
What You Get for Free
- Standard controls (
Button,Toggle,Slider,Picker,TextField,Link) come with correct labels, traits, and gestures. Image(systemName:)and labeledImage("name", label: Text(...))produce VoiceOver descriptions.Textis read verbatim; concatenated text is read in order.- Standard navigation (
NavigationStack, sheets, alerts) has correct focus management and hierarchy announcements.
What you have to do yourself:
- Label non-decorative images you create with
Image("…")orImage(uiImage:). - Label custom controls (anything you build with
onTapGestureor shapes). - Group related visuals so VoiceOver speaks them as a unit.
- Verify Dynamic Type, color, and motion behaviors.
- Provide
.accessibilityIdentifierfor UI tests when you need to find an element by id.
The Core Modifiers
| Modifier | Sets |
|---|---|
.accessibilityLabel(_:) | The "what is this" string VoiceOver reads |
.accessibilityValue(_:) | The current value (e.g., "75%") |
.accessibilityHint(_:) | Optional "double tap to ..." instruction |
.accessibilityAddTraits(_:) / .accessibilityRemoveTraits(_:) | .isButton, .isHeader, .isImage, .isSelected, .updatesFrequently, .isModal, .isSummaryElement |
.accessibilityHidden(_:) | Hide a decorative element from VoiceOver |
| `.accessibilityElement(children: .ignore | .combine |
.accessibilityAction(named:_:) | Add a custom rotor action |
.accessibilityRotor(_:entries:) | Make a custom rotor for jumping between items |
.accessibilitySortPriority(_:) | Force read order within a container |
.accessibilityIdentifier(_:) | UI-test identifier (not user-visible) |
.accessibilityRespondsToUserInteraction(_:) | Make a non-interactive element interactive for AT |
.accessibilityFocused($flag) | Programmatically move VoiceOver focus |
.accessibilityChildren { ... } | Provide synthetic children for a custom element |
Custom Controls
Anything that's tappable but not a Button needs explicit accessibility:
HStack { Image(systemName: "heart"); Text("Like") }
.onTapGesture { liked.toggle() }
.accessibilityElement(children: .combine)
.accessibilityAddTraits(.isButton)
.accessibilityLabel(liked ? "Liked" : "Like")
.accessibilityHint("Double tap to toggle")
Better: use a real Button with a custom label:
Button { liked.toggle() } label: {
HStack { Image(systemName: "heart"); Text("Like") }
}
Real Button gets .isButton, hit testing, and focus for free.
Grouping with accessibilityElement
By default, every leaf view in a VStack/HStack is its own VoiceOver element. For things that read better as a unit (a stat block, a row of related labels), combine them:
VStack {
Text("Distance")
Text("3.2 mi")
}
.accessibilityElement(children: .combine)
.accessibilityLabel("Distance, 3.2 miles")
Modes:
.ignore— treat children as decoration; the parent gets the label..combine— children's text is concatenated for the parent's label..contain— parent is a container; children remain individually navigable.
Images
Image("hero") // decorative? supply nothing OR hide:
.accessibilityHidden(true)
Image("hero", label: Text("Sunset over the bay")) // labeled
Image(decorative: "divider") // explicit decorative
For Image(systemName:), SF Symbols have built-in localized names — usually fine, but override when context demands ("envelope" → "Compose message").
Dynamic Type
Text, Label, TextField, and most controls scale automatically when the user adjusts text size. Things to watch for:
- Hard-coded
frameheights that clip text at large sizes. - Custom views drawing fixed-point text — use scaled metrics:
@ScaledMetric var iconSize: CGFloat = 24 Image(systemName: "star").font(.system(size: iconSize)) - Tests at the largest accessibility size:
.environment(\.dynamicTypeSize, .accessibility5)in previews. .lineLimit(1)+Textcan cut off the user's setting; consider wrapping.
Color, Contrast, and "Differentiate Without Color"
@Environment(\.accessibilityDifferentiateWithoutColor) var diffWithoutColor
@Environment(\.colorSchemeContrast) var contrast
@Environment(\.colorScheme) var scheme
Patterns:
- Use shape, position, or text in addition to color for status (✓ vs ✗ vs •).
- Test in Increase Contrast mode; ensure text remains legible.
- Don't rely on subtle hue differences for state — Color Filters and color blindness flatten them.
- Use semantic colors (
.primary,.secondary,.accentColor) so the system can adjust appearance.
Reduce Motion
@Environment(\.accessibilityReduceMotion) var reduceMotion
withAnimation(reduceMotion ? .none : .spring) { ... }
Adapt:
- Replace bouncy springs with crossfades.
- Skip parallax/zoom transitions.
- Avoid
repeatForeverambient motion.
Image(systemName:).symbolEffect(...) respects reduce-motion automatically.
VoiceOver Focus
Move focus programmatically when the UI changes context:
@AccessibilityFocusState var focusedField: Field?
TextField("Email", text: $email).accessibilityFocused($focusedField, equals: .email)
Button("Continue") {
if email.isEmpty { focusedField = .email }
}
Also use .accessibilityFocused after presenting a sheet to direct VoiceOver to the new content's heading.
Custom Actions and Rotors
For complex elements, add named actions instead of asking the user to find a button:
MessageRow(message: m)
.accessibilityAction(named: "Reply") { reply(m) }
.accessibilityAction(named: "Mark as unread") { markUnread(m) }
.accessibilityAction(.delete) { delete(m) }
Rotors let users jump through a category of items:
.accessibilityRotor("Headings") {
ForEach(headings) { h in
AccessibilityRotorEntry(h.title, id: h.id)
}
}
Identifiers for UI Testing
.accessibilityIdentifier("submit-button") is not read by VoiceOver — it's purely for XCUIElement lookup. Add identifiers to elements your UI tests need to interact with; don't repurpose accessibilityLabel for this.
Audit Checklist
Run this before declaring a view shippable:
- Every interactive element has a meaningful label or is a real
Button/Toggle. - Decorative images use
.accessibilityHidden(true)orImage(decorative:). - Related label/value pairs are combined via
accessibilityElement(children: .combine). - Headings use
.accessibilityAddTraits(.isHeader). - Selected state is communicated via
.isSelectedtrait, not just color. - Dynamic Type tested at
.accessibility3or higher; nothing clips. - Reduce Motion tested; no required-motion content.
- Differentiate Without Color tested; status conveyed by more than color.
- VoiceOver swipe order is logical (use
accessibilitySortPriorityif not). - After modal presentation, VoiceOver focus lands on the right element.
- All UI-test entry points have
accessibilityIdentifier. - Run Accessibility Inspector (Xcode → Open Developer Tool) and audit.
Previewing Accessibility
#Preview("A11y - Big Text") {
ContentView()
.environment(\.dynamicTypeSize, .accessibility3)
}
#Preview("A11y - VoiceOver-style") {
ContentView()
.environment(\.accessibilityEnabled, true)
.environment(\.accessibilityReduceMotion, true)
}
For audits, run on device with VoiceOver on (Settings → Accessibility → VoiceOver) — simulator support exists but device behavior is canonical.
Anti-Patterns
| Anti-pattern | Problem | Fix |
|---|---|---|
| Custom tappable view with no label | VoiceOver reads "button" with no context | .accessibilityLabel(...) or use a real Button |
Image("icon") with no label | "Image, icon" — useless | Provide label or hide if decorative |
| Status conveyed only by color | Invisible to color-blind users; flat under filters | Add shape/text/symbol |
.frame(height: 20) around Text | Truncates at large Dynamic Type | Let text size; use @ScaledMetric for icons |
| Three labels in a row left as separate elements | VoiceOver reads each separately, awkward | .accessibilityElement(children: .combine) |
Always-on repeatForever animation | Burns battery; ignores Reduce Motion | Gate on accessibilityReduceMotion |
Hijacking accessibilityLabel for UI test ids | Pollutes spoken UI | Use accessibilityIdentifier |
Heading styled with .font(.title) only | Not a heading semantically | Add .accessibilityAddTraits(.isHeader) |
| Sheet presented, focus stays on the presenter | User confused after sheet appears | @AccessibilityFocusState to set focus on sheet content |
Hidden via .opacity(0) | Still in accessibility tree | .accessibilityHidden(true) or remove from tree |
.accessibilityHidden(true) on a container | Hides all descendants from VoiceOver entirely | Apply selectively, or use .ignore mode in accessibilityElement |
Custom controls without .isButton trait | VoiceOver doesn't announce as button | Add the trait or use a real Button |
Gives 0 of the 12 instructions most accessibility skills give
Counted across 384 of the 385 authors here whose files we hold, read 2026-08-06
- maintain visible focus indicatorsin 46 of 384, across 33 files
- run automated accessibility scansin 44 of 384, across 32 files
- make all interactive elements keyboard reachablein 39 of 384, across 32 files
- use semantic HTML before ARIAin 38 of 384
- respect reduced motion preferencesin 35 of 384, across 27 files
- Associate labels programmatically with inputsin 33 of 384, across 26 files
- Use native elements over ARIAin 30 of 384, across 18 files
- test with a screen readerin 26 of 384, across 22 files
- map findings to WCAG criteriain 24 of 384, across 14 files
- meet minimum color contrast ratiosin 24 of 384, across 16 files
- Provide text alternatives for imagesin 24 of 384, across 18 files
- trap focus inside open modalsin 23 of 384, across 18 files
Said here and by no other author read
- label non-decorative images manually
- group related visuals for VoiceOver
- verify dynamic type and motion behaviors
- combine related label and value pairs
- apply header trait to semantic headings
- test dynamic type at large sizes
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once.