agentsclimarketplace

Swiftui accessibility

Skill Tyr0/agent-skills/plugins/swiftui-expert/skills/swiftui-accessibility

A collection of skills, plugins, and agents for AI workflows.

Install
npx -y skills add Tyr0/agent-skills --skill swiftui-accessibility

Assembled 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 labeled Image("name", label: Text(...)) produce VoiceOver descriptions.
  • Text is 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("…") or Image(uiImage:).
  • Label custom controls (anything you build with onTapGesture or shapes).
  • Group related visuals so VoiceOver speaks them as a unit.
  • Verify Dynamic Type, color, and motion behaviors.
  • Provide .accessibilityIdentifier for UI tests when you need to find an element by id.

The Core Modifiers

ModifierSets
.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 frame heights 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) + Text can 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 repeatForever ambient 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) or Image(decorative:).
  • Related label/value pairs are combined via accessibilityElement(children: .combine).
  • Headings use .accessibilityAddTraits(.isHeader).
  • Selected state is communicated via .isSelected trait, not just color.
  • Dynamic Type tested at .accessibility3 or 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 accessibilitySortPriority if 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-patternProblemFix
Custom tappable view with no labelVoiceOver reads "button" with no context.accessibilityLabel(...) or use a real Button
Image("icon") with no label"Image, icon" — uselessProvide label or hide if decorative
Status conveyed only by colorInvisible to color-blind users; flat under filtersAdd shape/text/symbol
.frame(height: 20) around TextTruncates at large Dynamic TypeLet text size; use @ScaledMetric for icons
Three labels in a row left as separate elementsVoiceOver reads each separately, awkward.accessibilityElement(children: .combine)
Always-on repeatForever animationBurns battery; ignores Reduce MotionGate on accessibilityReduceMotion
Hijacking accessibilityLabel for UI test idsPollutes spoken UIUse accessibilityIdentifier
Heading styled with .font(.title) onlyNot a heading semanticallyAdd .accessibilityAddTraits(.isHeader)
Sheet presented, focus stays on the presenterUser 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 containerHides all descendants from VoiceOver entirelyApply selectively, or use .ignore mode in accessibilityElement
Custom controls without .isButton traitVoiceOver doesn't announce as buttonAdd 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.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.