Use when fixing VoiceOver issues, Dynamic Type violations, color contrast failures, touch target problems, keyboard navigation gaps, or Reduce Motion support - comprehensive accessibility diagnostics with WCAG compliance, Accessibility Inspector workflows, and App Store Review preparation for iOS/macOS
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
Systematic accessibility diagnosis and remediation for iOS/macOS apps. Covers the 7 most common accessibility issues that cause App Store rejections and user complaints.
Core principle Accessibility is not optional. iOS apps must support VoiceOver, Dynamic Type, and sufficient color contrast to pass App Store Review. Users with disabilities depend on these features.
Problem Missing or generic accessibility labels prevent VoiceOver users from understanding UI purpose.
WCAG 4.1.2 Name, Role, Value (Level A)
// ❌ WRONG - No label (VoiceOver says "Button")
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
// ❌ WRONG - Generic label
.accessibilityLabel("Button")
// ❌ WRONG - Reads implementation details
.accessibilityLabel("cart.badge.plus") // VoiceOver: "cart dot badge dot plus"
// ✅ CORRECT - Descriptive label
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
.accessibilityLabel("Add to cart")
// ✅ CORRECT - With hint for complex actions
.accessibilityLabel("Add to cart")
.accessibilityHint("Double-tap to add this item to your shopping cart")
// ✅ CORRECT - Hide decorative images from VoiceOver
Image("decorative-pattern")
.accessibilityHidden(true)
// ✅ CORRECT - Combine multiple elements into one label
HStack {
Image(systemName: "star.fill")
Text("4.5")
Text("(234 reviews)")
}
.accessibilityElement(children: .combine)
.accessibilityLabel("Rating: 4.5 stars from 234 reviews")
Problem Fixed font sizes prevent users with vision disabilities from reading text.
WCAG 1.4.4 Resize Text (Level AA - support 200% scaling without loss of content/functionality)
// ❌ WRONG - Fixed size, won't scale
Text("Price: $19.99")
.font(.system(size: 17))
UILabel().font = UIFont.systemFont(ofSize: 17)
// ❌ WRONG - Custom font without scaling
Text("Headline")
.font(Font.custom("CustomFont", size: 24))
// ✅ CORRECT - SwiftUI semantic styles (auto-scales)
Text("Price: $19.99")
.font(.body)
Text("Headline")
.font(.headline)
// ✅ CORRECT - UIKit semantic styles
label.font = UIFont.preferredFont(forTextStyle: .body)
// ✅ CORRECT - Custom font with scaling
let customFont = UIFont(name: "CustomFont", size: 24)!
label.font = UIFontMetrics.default.scaledFont(for: customFont)
label.adjustsFontForContentSizeCategory = true
// ❌ WRONG - Fixed size, won't scale
Text("Price: $19.99")
.font(.system(size: 17))
// ⚠️ ACCEPTABLE - Custom font without scaling (accessibility violation)
Text("Headline")
.font(Font.custom("CustomFont", size: 24))
// ✅ GOOD - Custom size that scales with Dynamic Type
Text("Large Title")
.font(.system(size: 60).relativeTo(.largeTitle))
Text("Custom Headline")
.font(.system(size: 24).relativeTo(.title2))
// ✅ BEST - Use semantic styles when possible
Text("Headline")
.font(.headline)
How relativeTo: works
.title2, .largeTitle, etc.)Example
.title2 base: ~22pt → Your custom: 24pt (1.09x larger).title2 grows to ~28pt → Your custom grows to ~30.5pt (maintains 1.09x ratio)Fix hierarchy (best to worst)
.title, .body, .caption).system(size:).relativeTo() for required custom sizes.dynamicTypeSize() modifier.largeTitle - 34pt (scales to 44pt at accessibility sizes).title - 28pt.title2 - 22pt.title3 - 20pt.headline - 17pt semibold.body - 17pt (default).callout - 16pt.subheadline - 15pt.footnote - 13pt.caption - 12pt.caption2 - 11pt// ❌ WRONG - Fixed frame breaks with large text
Text("Long product description...")
.font(.body)
.frame(height: 50) // Clips at large text sizes
// ✅ CORRECT - Flexible frame
Text("Long product description...")
.font(.body)
.lineLimit(nil) // Allow multiple lines
.fixedSize(horizontal: false, vertical: true)
// ✅ CORRECT - Stack rearranges at large sizes
HStack {
Text("Label:")
Text("Value")
}
.dynamicTypeSize(...DynamicTypeSize.xxxLarge) // Limit maximum size if needed
Xcode Preview: Environment override
.environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)
Simulator: Settings → Accessibility → Display & Text Size → Larger Text → Drag to maximum
Device: Settings → Accessibility → Display & Text Size → Larger Text
Check: Does text remain readable? Does layout adapt? Is any text clipped?
Problem Low contrast text is unreadable for users with vision disabilities or in bright sunlight.
// ❌ WRONG - Low contrast (1.8:1 - fails WCAG)
Text("Warning")
.foregroundColor(.yellow) // on white background
// ❌ WRONG - Low contrast in dark mode
Text("Info")
.foregroundColor(.gray) // on black background
// ✅ CORRECT - High contrast (7:1+ passes AAA)
Text("Warning")
.foregroundColor(.orange) // or .red
// ✅ CORRECT - System colors adapt to light/dark mode
Text("Info")
.foregroundColor(.primary) // Black in light mode, white in dark
Text("Secondary")
.foregroundColor(.secondary) // Automatic high contrast
// ❌ WRONG - Color alone indicates status
Circle()
.fill(isAvailable ? .green : .red)
// ✅ CORRECT - Color + icon/text
HStack {
Image(systemName: isAvailable ? "checkmark.circle.fill" : "xmark.circle.fill")
Text(isAvailable ? "Available" : "Unavailable")
}
.foregroundColor(isAvailable ? .green : .red)
// ✅ CORRECT - Respect system preference
if UIAccessibility.shouldDifferentiateWithoutColor {
// Use patterns, icons, or text instead of color alone
}
Problem Small tap targets are difficult or impossible for users with motor disabilities.
WCAG 2.5.5 Target Size (Level AAA - 44x44pt minimum)
Apple HIG 44x44pt minimum for all tappable elements
// ❌ WRONG - Too small (24x24pt)
Button("×") {
dismiss()
}
.frame(width: 24, height: 24)
// ❌ WRONG - Small icon without padding
Image(systemName: "heart")
.font(.system(size: 16))
.onTapGesture { }
// ✅ CORRECT - Minimum 44x44pt
Button("×") {
dismiss()
}
.frame(minWidth: 44, minHeight: 44)
// ✅ CORRECT - Larger icon or padding
Image(systemName: "heart")
.font(.system(size: 24))
.frame(minWidth: 44, minHeight: 44)
.contentShape(Rectangle()) // Expand tap area
.onTapGesture { }
// ✅ CORRECT - UIKit button with edge insets
button.contentEdgeInsets = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
// Total size: icon size + insets ≥ 44x44pt
// ❌ WRONG - Targets too close (hard to tap accurately)
HStack(spacing: 4) {
Button("Edit") { }
Button("Delete") { }
}
// ✅ CORRECT - Adequate spacing (8pt minimum, 12pt better)
HStack(spacing: 12) {
Button("Edit") { }
Button("Delete") { }
}
Problem Users who cannot use touch/mouse cannot navigate app.
WCAG 2.1.1 Keyboard (Level A - all functionality available via keyboard)
// ❌ WRONG - Custom gesture without keyboard alternative
.onTapGesture {
showDetails()
}
// No way to trigger with keyboard
// ✅ CORRECT - Button provides keyboard support automatically
Button("Show Details") {
showDetails()
}
.keyboardShortcut("d", modifiers: .command) // Optional shortcut
// ✅ CORRECT - Custom control with focus support
struct CustomButton: View {
@FocusState private var isFocused: Bool
var body: some View {
Text("Custom")
.focusable()
.focused($isFocused)
.onKeyPress(.return) {
action()
return .handled
}
}
}
// ✅ CORRECT - Set initial focus
.focusSection() // Group related controls
.defaultFocus($focus, .constant(true)) // Set default
// ✅ CORRECT - Move focus after action
@FocusState private var focusedField: Field?
Button("Next") {
focusedField = .next
}
Problem Animations cause discomfort, nausea, or seizures for users with vestibular disorders.
WCAG 2.3.3 Animation from Interactions (Level AAA - motion animation can be disabled)
// ❌ WRONG - Always animates (can cause nausea)
.onAppear {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
scale = 1.0
}
}
// ❌ WRONG - Parallax scrolling without opt-out
ScrollView {
GeometryReader { geo in
Image("hero")
.offset(y: geo.frame(in: .global).minY * 0.5) // Parallax
}
}
// ✅ CORRECT - Respect Reduce Motion preference
.onAppear {
if UIAccessibility.isReduceMotionEnabled {
scale = 1.0 // Instant
} else {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
scale = 1.0
}
}
}
// ✅ CORRECT - Simpler animation or cross-fade
if UIAccessibility.isReduceMotionEnabled {
// Cross-fade or instant change
withAnimation(.linear(duration: 0.2)) {
showView = true
}
} else {
// Complex spring animation
withAnimation(.spring()) {
showView = true
}
}
// ✅ CORRECT - Automatic support
.animation(.spring(), value: isExpanded)
.transaction { transaction in
if UIAccessibility.isReduceMotionEnabled {
transaction.animation = nil // Disable animation
}
}
// ❌ WRONG - Informative image without label
Image("product-photo")
// ✅ CORRECT - Informative image with label
Image("product-photo")
.accessibilityLabel("Red sneakers with white laces")
// ✅ CORRECT - Decorative image hidden
Image("background-pattern")
.accessibilityHidden(true)
// ❌ WRONG - Custom button without button trait
Text("Submit")
.onTapGesture {
submit()
}
// VoiceOver announces as "Submit, text" not "Submit, button"
// ✅ CORRECT - Use Button for button-like controls
Button("Submit") {
submit()
}
// VoiceOver announces as "Submit, button"
// ✅ CORRECT - Custom control with correct trait
Text("Submit")
.accessibilityAddTraits(.isButton)
.onTapGesture {
submit()
}
// ❌ WRONG - Custom slider without accessibility support
struct CustomSlider: View {
@Binding var value: Double
var body: some View {
// Drag gesture only, no VoiceOver support
GeometryReader { geo in
// ...
}
.gesture(DragGesture()...)
}
}
// ✅ CORRECT - Custom slider with accessibility actions
struct CustomSlider: View {
@Binding var value: Double
var body: some View {
GeometryReader { geo in
// ...
}
.gesture(DragGesture()...)
.accessibilityElement()
.accessibilityLabel("Volume")
.accessibilityValue("\(Int(value))%")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment:
value = min(value + 10, 100)
case .decrement:
value = max(value - 10, 0)
@unknown default:
break
}
}
}
}
// ❌ WRONG - State change without announcement
Button("Toggle") {
isOn.toggle()
}
// ✅ CORRECT - State change with announcement
Button("Toggle") {
isOn.toggle()
UIAccessibility.post(
notification: .announcement,
argument: isOn ? "Enabled" : "Disabled"
)
}
// ✅ CORRECT - Automatic state with accessibilityValue
Button("Toggle") {
isOn.toggle()
}
.accessibilityValue(isOn ? "Enabled" : "Disabled")
Xcode → Open Developer Tool → Accessibility Inspector
.accessibilityAdjustableAction)VoiceOver Support
Dynamic Type
Sufficient Contrast
When submitting:
Accessibility → Select features your app supports:
Test Notes: Document accessibility testing
Accessibility Testing Completed:
- VoiceOver: All screens tested with VoiceOver enabled
- Dynamic Type: Tested at all size categories
- Color Contrast: Verified 4.5:1 minimum contrast
- Touch Targets: All buttons minimum 44x44pt
- Reduce Motion: Animations respect user preference
"App is not fully functional with VoiceOver"
"Text is not readable at all Dynamic Type sizes"
"Insufficient color contrast"
Under design review pressure, you'll face requests to:
These sound like reasonable design preferences. But they violate App Store requirements and exclude 15% of users. Your job: defend using App Store guidelines and legal requirements, not opinion.
If you hear ANY of these, STOP and reference this skill:
"I want to support this design direction, but let me show you Apple's App Store
Review Guideline 2.5.1:
'Apps should support accessibility features such as VoiceOver and Dynamic Type.
Failure to include sufficient accessibility features may result in rejection.'
Here's what we need for approval:
1. VoiceOver labels on all interactive elements
2. Dynamic Type support (can't lock font sizes)
3. 4.5:1 contrast ratio for text, 3:1 for UI
4. 44x44pt minimum touch targets
Let me show where our design currently falls short..."
Open the app with accessibility features enabled:
"I can achieve your aesthetic goals while meeting accessibility requirements:
1. VoiceOver labels: Add them programmatically (invisible in UI, required for approval)
2. Dynamic Type: Use layout techniques that adapt (examples from Apple HIG)
3. Contrast: Adjust colors slightly to meet 4.5:1 (I'll show options that preserve brand)
4. Touch targets: Expand hit areas programmatically (visual size stays the same)
These changes won't affect the visual design you're seeing, but they're required
for App Store approval and legal compliance."
If overruled (designer insists on violations):
Slack message to PM + designer:
"Design review decided to proceed with:
- Fixed font sizes (disabling Dynamic Type)
- 38x38pt buttons (below 44pt requirement)
- 3.8:1 text contrast (below 4.5:1 requirement)
Important: These changes violate App Store Review Guideline 2.5.1 and WCAG AA.
This creates three risks:
1. App Store rejection during review (adds 1-2 week delay)
2. ADA compliance issues if user files complaint (legal risk)
3. 15% of potential users unable to use app effectively
I'm flagging this proactively so we can prepare a response plan if rejected."
// ❌ WRONG - Generic labels (will fail re-review)
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
.accessibilityLabel("Button") // Apple will reject again
// ✅ CORRECT - Descriptive labels (passes review)
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
.accessibilityLabel("Add to cart")
.accessibilityHint("Double-tap to add this item to your shopping cart")
Time estimate 2-4 hours to audit all interactive elements and add proper labels.
Sometimes designers have valid reasons to override accessibility guidelines. Accept if:
"Design review decided to proceed with [specific violations].
We understand this creates:
- App Store rejection risk (Guideline 2.5.1)
- Potential 1-2 week delay if rejected
- Need to audit and fix all instances if rejected
Monitoring plan:
- Submit for review with current design
- If rejected, implement proper accessibility (estimated 2-4 hours)
- Have accessibility-compliant version ready as backup"
This protects both of you and shows you're not blocking - just de-risking.
Goal Meet Level AA for all content, Level AAA where feasible.
After making fixes:
# Quick scan for new issues
/axiom:audit-accessibility
# Deep diagnosis for specific issues
/skill axiom:accessibility-diag
Remember Accessibility is not a feature, it's a requirement. 15% of users have some form of disability. Making your app accessible isn't just the right thing to do - it expands your user base and improves the experience for everyone.