Help us improve
Share bugs, ideas, or general feedback.
From swift-accessibility-plugin
Audit, fix, and initialise SwiftUI accessibility modifiers so your app is navigable by both VoiceOver users and AI agents. Use this skill whenever a user mentions accessibility audit, accessibility modifiers, making an iOS app navigable by agents, adding VoiceOver support, or coordinate tracking for SwiftUI.
npx claudepluginhub conorluddy/skills --plugin swift-accessibility-pluginHow this skill is triggered — by the user, by Claude, or both
Slash command
/swift-accessibility-plugin:swift-accessibility-agentThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Make SwiftUI apps fully navigable by VoiceOver, XCTest, and AI agents by ensuring every
Creates p5.js generative art with seeded randomness, noise fields, and interactive parameter exploration. Use for algorithmic art, flow fields, or particle systems.
Share bugs, ideas, or general feedback.
Make SwiftUI apps fully navigable by VoiceOver, XCTest, and AI agents by ensuring every interactive element carries the five accessibility properties: identifier, label, hint, value, and traits.
Most AI agents navigate iOS apps via screenshots — slow (~2-5s per step), expensive (~1,600 image tokens per screenshot), and fragile. A fully populated accessibility tree lets agents query structured text (~200-400 tokens), tap by identifier (deterministic), and verify via logs — no vision model needed. The same work also makes the app properly accessible to humans using VoiceOver, Switch Control, and Voice Control.
The user will tell you what they want, or you can suggest the right mode based on context.
init — Scaffold CoordinateTrackerCreates the CoordinateTracker.swift file in the project. This is the infrastructure
that lets agents query exact screen coordinates for any tracked element without screenshots.
When to use: First time setting up a project for agent navigation, or when the user says "init", "set up tracking", or "add coordinate tracker".
Steps:
Sources/, App/, etc.) — or detect
the most likely location by looking for existing .swift filesCoordinateTracker.swift already exists anywhere in the projectaudit — Report accessibility gapsScans SwiftUI files and reports which interactive elements are missing accessibility modifiers, without changing any code.
When to use: The user wants to understand current coverage before making changes, or says "audit", "check accessibility", "what's missing".
Steps:
.swift files in scope## Accessibility Audit Report
### file: Views/SessionTimerView.swift
| Line | Element | Type | identifier | label | hint | value | traits |
|------|---------|------|:---:|:---:|:---:|:---:|:---:|
| 23 | "Save" | Button | — | — | — | n/a | auto |
| 45 | HStack | List row | — | — | — | — | — |
| 67 | Toggle | Toggle | — | OK | — | — | auto |
### Summary
- Files scanned: 12
- Interactive elements found: 34
- Fully accessible: 8 (24%)
- Missing identifiers: 26
- Missing labels: 18
- Missing hints: 22
- Missing values: 14 (of elements that carry state)
Important: value only applies to elements that carry state (Toggle, Picker,
Slider, Stepper, list rows with data, progress indicators). Don't flag buttons or
navigation links as missing value unless they have dynamic state. traits are
often inferred automatically by SwiftUI (Button gets .button, etc.) — only flag
when traits are ambiguous or missing (e.g. a tappable HStack that should be marked
as a button).
fix — Add missing accessibility modifiersReads each file, identifies gaps, and adds the appropriate modifiers. This is the main workhorse mode.
When to use: The user wants to actually improve their code, or says "fix", "add modifiers", "make accessible", "augment".
Steps:
--track or "with tracking" is mentioned, also add .trackElement() calls
(requires init to have been run first — check for CoordinateTracker.swift)These SwiftUI elements need accessibility modifiers when interactive or informational:
Button / Button(action:) / .onTapGestureNavigationLinkTogglePicker / DatePickerSliderStepperTextField / SecureField / TextEditorLinkMenuHStack / VStack / ZStack used as list rows (look for onTapGesture,
NavigationLink wrapping, or List { ... } context)Image that conveys meaning (not decorative)Label when used standaloneText that displays dynamic state.accessibilityHidden(true))Image(systemName: "chevron.right") disclosure indicatorsScrollView, List, Form, NavigationStack — the top-level container of each
screen should have .accessibilityIdentifier("screen_name_view") so agents can
orient themselvesUse this structured pattern for identifiers:
{category}_{context}_{element}_{modifier?}
technique, session, position, settings, navigation)editor, list, detail, timer, tab_bar)button, row, textfield, toggle, picker)save, delete, name, filter)Examples:
"technique_editor_save_button"
"position_list_row_\(position.id)"
"session_timer_start_button"
"navigation_tab_bar_training"
"form_textfield_technique_name"
"settings_notifications_toggle"
Infer category and context from the file name, containing view struct, and
surrounding code. The identifier should be self-describing — someone reading
"technique_editor_save_button" in a log should immediately know the domain,
screen, and element without looking up code.
.accessibilityLabel())"Save technique", "Guard position", "Session duration""Button", "MarqueeText", "Blue circle".accessibilityHint())"Validates and stores the current technique""Tap to save" (VoiceOver already tells users to tap).accessibilityValue())"3 of 5 selected", "On", "Page 2 of 4", "\(position.transitionCount) transitions"Add modifiers directly after the element, before any layout modifiers like .padding()
or .frame(). Group accessibility modifiers together:
Button("Save") {
saveTechnique()
}
.accessibilityIdentifier("technique_editor_save_button")
.accessibilityLabel("Save technique")
.accessibilityHint("Validates and stores the current technique")
.padding()
.frame(maxWidth: .infinity)
For list rows, apply modifiers to the outermost container and hide decorative children:
HStack(spacing: 12) {
Circle().fill(.blue).frame(width: 8)
.accessibilityHidden(true)
VStack(alignment: .leading) {
Text(position.name)
Text("\(position.transitionCount) transitions")
.foregroundStyle(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.accessibilityHidden(true)
}
.accessibilityIdentifier("position_list_row_\(position.id)")
.accessibilityLabel(position.name)
.accessibilityHint("Opens detailed information for \(position.name)")
.accessibilityValue("\(position.transitionCount) transitions")
.trackElement() (opt-in)Only add .trackElement() when the user explicitly opts in (says "with tracking",
passes --track, or has run init). When adding it, use the same string as the
accessibilityIdentifier:
Button("Start session") { startSession() }
.accessibilityIdentifier("session_timer_start_button")
.accessibilityLabel("Start training session")
.accessibilityHint("Begins a new timed training session")
.trackElement("session_timer_start_button")
Drop this into your project as CoordinateTracker.swift during init mode:
import SwiftUI
@MainActor
final class CoordinateTracker: ObservableObject {
static let shared = CoordinateTracker()
private init() {}
struct TrackedElement {
let id: String
let frame: CGRect
var center: CGPoint { CGPoint(x: frame.midX, y: frame.midY) }
}
private(set) var elements: [String: TrackedElement] = [:]
private(set) var currentView: String?
private(set) var viewMetadata: [String: String] = [:]
func track(id: String, frame: CGRect) {
elements[id] = TrackedElement(id: id, frame: frame)
}
func tapPoint(for id: String) -> CGPoint? {
elements[id]?.center
}
func updateViewContext(viewName: String, metadata: [String: String] = [:]) {
currentView = viewName
viewMetadata = metadata
}
}
extension View {
func trackElement(_ id: String) -> some View {
background(
GeometryReader { geo in
Color.clear.onAppear {
CoordinateTracker.shared.track(
id: id,
frame: geo.frame(in: .global)
)
}
}
)
}
}
Update view context on screen appear:
.onAppear {
CoordinateTracker.shared.updateViewContext(
viewName: "SessionTimerView",
metadata: ["sessionId": session.id]
)
}
After fixing a file, verify:
identifier + labelhintvalue