Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Mobile accessibility testing skill for WCAG compliance, VoiceOver/TalkBack validation, dynamic type support, color contrast analysis, and accessibility auditing across iOS and Android platforms.
.claude/skills/a5c-ai-accessibility-testing/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-12 | ✗→✓ | ▲ Improved | 31% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 48% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 301% | 0% |
| case-20 | ✗→✓ | ▲ Improved | 118% | 0% |
| case-21 | ✓→✗ | ▼ Worse | 104% | 0% |
Comprehensive mobile accessibility testing and validation for iOS and Android platforms, ensuring WCAG 2.1/2.2 compliance and optimal screen reader compatibility.
This skill provides capabilities for testing mobile application accessibility, including screen reader compatibility, dynamic type support, color contrast validation, and compliance with Web Content Accessibility Guidelines (WCAG) adapted for mobile platforms.
bash# Accessibility testing tools xcode-select --install # UI testing with accessibility focus pod 'ViewInspector' # SwiftUI testing
groovy// build.gradle dependencies { androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.5.1' }
bash# Accessibility testing CLI tools npm install -g @axe-core/cli pip install accessibility-checker
swiftimport SwiftUI struct AccessibleButton: View { var body: some View { Button(action: { /* action */ }) { Image(systemName: "heart.fill") } .accessibilityLabel("Add to favorites") .accessibilityHint("Double tap to add this item to your favorites list") .accessibilityAddTraits(.isButton) } } struct AccessibleList: View { var body: some View { List { ForEach(items) { item in ItemRow(item: item) .accessibilityElement(children: .combine) .accessibilityLabel("\(item.title), \(item.subtitle)") .accessibilityValue(item.isSelected ? "Selected" : "Not selected") } } .accessibilityIdentifier("items_list") } }
swiftimport UIKit class AccessibleViewController: UIViewController { func configureAccessibility() { // Basic label button.accessibilityLabel = "Submit order" button.accessibilityHint = "Double tap to submit your order" // Grouped elements containerView.isAccessibilityElement = true containerView.accessibilityLabel = "Order summary: 3 items, total $45.99" // Custom actions cell.accessibilityCustomActions = [ UIAccessibilityCustomAction(name: "Delete", target: self, selector: #selector(deleteItem)), UIAccessibilityCustomAction(name: "Edit", target: self, selector: #selector(editItem)) ] } }
kotlinimport androidx.compose.ui.semantics.* @Composable fun AccessibleButton() { IconButton( onClick = { /* action */ }, modifier = Modifier.semantics { contentDescription = "Add to favorites" role = Role.Button } ) { Icon(Icons.Filled.Favorite, contentDescription = null) } } @Composable fun AccessibleCard(item: Item) { Card( modifier = Modifier.semantics(mergeDescendants = true) { contentDescription = "${item.title}, ${item.subtitle}" stateDescription = if (item.isSelected) "Selected" else "Not selected" } ) { // Card content } }
kotlinimport android.view.View import android.view.accessibility.AccessibilityNodeInfo class AccessibleActivity : AppCompatActivity() { fun configureAccessibility() { // Basic content description imageButton.contentDescription = "Add to favorites" // Important for accessibility decorativeImage.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO // Live regions for dynamic content statusTextView.accessibilityLiveRegion = View.ACCESSIBILITY_LIVE_REGION_POLITE // Custom accessibility delegate customView.accessibilityDelegate = object : View.AccessibilityDelegate() { override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) { super.onInitializeAccessibilityNodeInfo(host, info) info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK) info.contentDescription = "Custom description" } } } }
swift// iOS - Check contrast ratio import UIKit func calculateContrastRatio(foreground: UIColor, background: UIColor) -> Double { let fgLuminance = relativeLuminance(foreground) let bgLuminance = relativeLuminance(background) let lighter = max(fgLuminance, bgLuminance) let darker = min(fgLuminance, bgLuminance) return (lighter + 0.05) / (darker + 0.05) } func relativeLuminance(_ color: UIColor) -> Double { var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0 color.getRed(&r, green: &g, blue: &b, alpha: nil) let transform: (CGFloat) -> Double = { value in let v = Double(value) return v <= 0.03928 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4) } return 0.2126 * transform(r) + 0.7152 * transform(g) + 0.0722 * transform(b) } // Usage let ratio = calculateContrastRatio(foreground: .label, background: .systemBackground) let meetsWCAGAA = ratio >= 4.5 // Normal text let meetsWCAGAAA = ratio >= 7.0 // Enhanced
swiftimport XCTest class AccessibilityTests: XCTestCase { func testVoiceOverNavigation() { let app = XCUIApplication() app.launch() // Verify accessibility elements exist XCTAssertTrue(app.buttons["Submit order"].exists) XCTAssertTrue(app.staticTexts["Order total"].exists) // Check accessibility traits let submitButton = app.buttons["Submit order"] XCTAssertTrue(submitButton.isEnabled) // Navigate with VoiceOver gestures (simulated) let elements = app.descendants(matching: .any).allElementsBoundByAccessibilityElement XCTAssertGreaterThan(elements.count, 0) } func testDynamicTypeSupport() { let app = XCUIApplication() app.launchArguments = ["-UIPreferredContentSizeCategoryName", "UICTContentSizeCategoryAccessibilityXXL"] app.launch() // Verify layout doesn't break at large text sizes XCTAssertTrue(app.staticTexts["Title"].exists) XCTAssertFalse(app.staticTexts["Title"].frame.isEmpty) } }
kotlinimport androidx.test.espresso.accessibility.AccessibilityChecks import org.junit.BeforeClass class AccessibilityTest { companion object { @BeforeClass @JvmStatic fun enableAccessibilityChecks() { AccessibilityChecks.enable() .setRunChecksFromRootView(true) } } @Test fun testScreenAccessibility() { onView(withId(R.id.main_layout)) .check(matches(isDisplayed())) // Automatic accessibility checks run on every view interaction onView(withId(R.id.submit_button)) .perform(click()) } }
javascriptconst accessibilityTestTask = defineTask({ name: 'accessibility-testing', description: 'Test mobile app accessibility compliance', inputs: { platform: { type: 'string', required: true, enum: ['ios', 'android', 'both'] }, wcagLevel: { type: 'string', required: true, enum: ['A', 'AA', 'AAA'] }, projectPath: { type: 'string', required: true }, testScreens: { type: 'array', items: { type: 'string' } } }, outputs: { complianceReport: { type: 'object' }, violations: { type: 'array' }, recommendations: { type: 'array' }, score: { type: 'number' } }, async run(inputs, taskCtx) { return { kind: 'skill', title: `Test ${inputs.wcagLevel} accessibility for ${inputs.platform}`, skill: { name: 'accessibility-testing', context: { operation: 'audit', platform: inputs.platform, wcagLevel: inputs.wcagLevel, projectPath: inputs.projectPath, screens: inputs.testScreens } }, io: { inputJsonPath: `tasks/${taskCtx.effectId}/input.json`, outputJsonPath: `tasks/${taskCtx.effectId}/result.json` } }; } });
json{ "mcpServers": { "axiom": { "command": "npx", "args": ["axiom-mcp"], "env": { "XCODE_PROJECT": "/path/to/project.xcodeproj" } } } }
a11y_audit_ios - Run iOS Accessibility Inspector audita11y_audit_android - Run Android Accessibility Scannercheck_contrast_ratio - Validate color contrastvalidate_touch_targets - Check touch target sizestest_screen_reader - Simulate screen reader navigationgenerate_a11y_report - Create compliance report| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 23,808 | 30,186 | +27% | 1 | 1 | 0% | 3,038 | 8,612 | +183% | 0 | 0 | — |
case-02 | pass→pass | 13,846 | 15,636 | +13% | 1 | 1 | 0% | 2,091 | 4,709 | +125% | 0 | 0 | — |
case-03 | pass→pass | 9,673 | 10,449 | +8% | 1 | 1 | 0% | 1,423 | 4,640 | +226% | 0 | 0 | — |
case-04 | pass→pass | 7,168 | 9,692 | +35% | 1 | 1 | 0% | 1,250 | 5,012 | +301% | 0 | 0 | — |
case-05 | pass→pass | 4,382 | 6,165 | +41% | 1 | 1 | 0% | 602 | 3,972 | +560% | 0 | 0 | — |
case-06 | pass→pass | 13,092 | 10,931 | -17% | 1 | 1 | 0% | 1,925 | 5,201 | +170% | 0 | 0 | — |
case-07 | pass→pass | 13,963 | 10,291 | -26% | 1 | 1 | 0% | 1,961 | 5,004 | +155% | 0 | 0 | — |
case-08 | pass→pass | 13,284 | 15,219 | +15% | 1 | 1 | 0% | 2,316 | 5,309 | +129% | 0 | 0 | — |
case-09 | pass→pass | 14,147 | 6,849 | -52% | 1 | 1 | 0% | 2,090 | 4,065 | +94% | 0 | 0 | — |
case-10 | pass→pass | 7,627 | 4,726 | -38% | 1 | 1 | 0% | 1,060 | 3,918 | +270% | 0 | 0 | — |
case-11 | pass→pass | 14,029 | 13,547 | -3% | 1 | 1 | 0% | 2,369 | 5,041 | +113% | 0 | 0 | — |
case-12 | fail→pass | 20,367 | 4,558 | -78% | 1 | 1 | 0% | 3,030 | 3,978 | +31% | 0 | 0 | — |
case-13 | fail→pass | 33,944 | 16,930 | -50% | 1 | 1 | 0% | 3,939 | 5,815 | +48% | 0 | 0 | — |
case-14 | pass→pass | 8,912 | 14,130 | +59% | 1 | 1 | 0% | 1,903 | 5,377 | +183% | 0 | 0 | — |
case-15 | pass→pass | 10,797 | 7,711 | -29% | 1 | 1 | 0% | 1,634 | 4,518 | +176% | 0 | 0 | — |
case-16 | pass→pass | 7,495 | 10,547 | +41% | 1 | 1 | 0% | 1,283 | 4,575 | +257% | 0 | 0 | — |
case-17 | pass→pass | 6,221 | 4,704 | -24% | 1 | 1 | 0% | 989 | 3,841 | +288% | 0 | 0 | — |
case-18 | fail→pass | 6,839 | 4,385 | -36% | 1 | 1 | 0% | 958 | 3,845 | +301% | 0 | 0 | — |
case-19 | pass→pass | 7,999 | 11,436 | +43% | 1 | 1 | 0% | 1,443 | 4,791 | +232% | 0 | 0 | — |
case-20 | fail→pass | 14,134 | 19,099 | +35% | 1 | 1 | 0% | 2,700 | 5,887 | +118% | 0 | 0 | — |
case-21 | pass→fail | 19,687 | 16,142 | -18% | 1 | 1 | 0% | 2,944 | 6,012 | +104% | 0 | 0 | — |
case-22 | pass→fail | 20,362 | 23,868 | +17% | 1 | 1 | 0% | 3,085 | 7,256 | +135% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.