Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/tomevault-io/skills-registry --skill mobile-accessibility명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SKILL.md 표시 중
SOC 직업 분류 기준
| name | mobile-accessibility |
| description | > Use when this capability is needed. |
You are a senior mobile engineer with accessibility expertise. Help the user implement, audit, and fix accessibility in mobile apps to ensure they are usable by everyone, including people with visual, motor, auditory, and cognitive disabilities.
| Question | Why It Matters |
|---|---|
| What platform? (Flutter, Android, iOS) | Platform-specific APIs and tools |
| What are the target compliance standards? (WCAG 2.1 AA, Section 508, EN 301 549) | Determines minimum requirements |
| Is this a new implementation or fixing existing issues? | Greenfield vs. remediation |
| What types of disabilities are most critical? (vision, motor, cognitive) | Prioritization focus |
| Is the app in a regulated industry? (government, banking, healthcare) | May have legal a11y requirements |
| Principle | What It Means | Implementation |
|---|---|---|
| Perceivable | Users can perceive all content | Labels, contrast, alt text, captions |
| Operable | Users can interact with all controls | Touch targets, keyboard, focus order |
| Understandable | Users can understand content and UI | Clear language, predictable behavior |
| Robust | Works with assistive technologies | Semantic markup, standard controls |
// Basic semantic label
Semantics(
label: 'Add item to cart',
button: true,
child: IconButton(
icon: const Icon(Icons.add_shopping_cart),
onPressed: () => addToCart(),
),
)
// Exclude decorative elements
Semantics(
excludeSemantics: true, // screen reader ignores this
child: Image.asset('assets/decorative_divider.png'),
)
// Group related content
MergeSemantics(
child: Row(
children: [
const Icon(Icons.star, color: Colors.amber),
Text('4.5 out of 5 stars'),
],
),
)
// Custom semantic actions
Semantics(
label: 'Product: Running Shoes, \$99.99',
customSemanticsActions: {
CustomSemanticsAction(label: 'Add to cart'): () => addToCart(),
CustomSemanticsAction(label: 'Add to wishlist'): () => addToWishlist(),
},
child: ProductCard(product: product),
)
// Announce dynamic changes
SemanticsService.announce('Item added to cart', TextDirection.ltr);
// Live region for dynamic updates (auto-announced)
Semantics(
liveRegion: true,
child: Text('$cartCount items in cart'),
)
Flutter a11y widgets:
| Widget | Purpose |
|---|---|
Semantics | Add labels, hints, traits to any widget |
MergeSemantics | Group related widgets into one semantic node |
ExcludeSemantics | Hide decorative elements from screen reader |
SemanticsService.announce() | Announce dynamic changes |
FocusTraversalGroup | Control focus/swipe order |
Tooltip | Adds long-press hint and screen reader label |
// Content description for images/icons
imageView.contentDescription = "Product photo: Running Shoes"
// Compose — semantic properties
Icon(
Icons.Default.ShoppingCart,
contentDescription = "Add to cart", // required for interactive icons
modifier = Modifier.semantics {
role = Role.Button
stateDescription = "2 items in cart"
}
)
// Compose — merge semantics for grouped content
Row(modifier = Modifier.semantics(mergeDescendants = true) {
contentDescription = "Product: Running Shoes, $99.99, 4.5 stars"
}) {
Text("Running Shoes")
Text("$99.99")
RatingBar(rating = 4.5f)
}
// Live region for dynamic updates
Text(
"$cartCount items",
modifier = Modifier.semantics {
liveRegion = LiveRegionMode.Polite
}
)
// Announce to screen reader
view.announceForAccessibility("Item added to cart")
// Heading for navigation
Text(
"Product Details",
modifier = Modifier.semantics { heading() }
)
// Custom actions
Modifier.semantics {
customActions = listOf(
CustomAccessibilityAction("Add to cart") { addToCart(); true },
CustomAccessibilityAction("Share") { share(); true }
)
}
// Content description
imageView.setContentDescription("Product photo: Running Shoes");
// Importrance for accessibility
decorativeView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
// Live region
cartCountView.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
// Custom accessibility delegate
ViewCompat.setAccessibilityDelegate(productCard, new AccessibilityDelegateCompat() {
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
info.addAction(new AccessibilityActionCompat(
AccessibilityActionCompat.ACTION_CLICK, "Add to cart"));
info.setRoleDescription("Product card");
}
});
// SwiftUI — accessibility modifiers
Image("running_shoes")
.accessibilityLabel("Product photo: Running Shoes")
// Hide decorative elements
Image("divider")
.accessibilityHidden(true)
// Group related content
HStack {
Text("Running Shoes")
Text("$99.99")
}
.accessibilityElement(children: .combine) // read as one element
// Custom label for complex views
ProductCard(product: product)
.accessibilityElement(children: .ignore)
.accessibilityLabel("Running Shoes, $99.99, 4.5 out of 5 stars")
.accessibilityAddTraits(.isButton)
.accessibilityAction(named: "Add to cart") { addToCart() }
.accessibilityAction(named: "Add to wishlist") { addToWishlist() }
// Heading for navigation
Text("Product Details")
.accessibilityAddTraits(.isHeader)
// Announce dynamic changes
UIAccessibility.post(notification: .announcement, argument: "Item added to cart")
// Value for controls (slider, stepper)
Stepper("Quantity: \(quantity)", value: $quantity, in: 1...10)
.accessibilityValue("\(quantity) items")
// Sort priority (control reading order)
VStack {
Text("Important")
.accessibilitySortPriority(1)
Text("Secondary")
.accessibilitySortPriority()
}
// Basic label
imageView.accessibilityLabel = @"Product photo: Running Shoes";
imageView.isAccessibilityElement = YES;
// Hide decorative elements
decorativeView.isAccessibilityElement = NO;
decorativeView.accessibilityElementsHidden = YES;
// Traits
button.accessibilityTraits = UIAccessibilityTraitButton;
header.accessibilityTraits = UIAccessibilityTraitHeader;
// Announce changes
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, @"Item added to cart");
// Custom actions
productCard.accessibilityCustomActions = @[
[[UIAccessibilityCustomAction alloc] initWithName:@"Add to cart"
target:self
selector:@selector(addToCart)],
[[UIAccessibilityCustomAction alloc] initWithName:@"Share"
target:self
selector:@selector(share)]
];
// Container with custom focus order
- (NSArray *)accessibilityElements {
return @[self.titleLabel, self.priceLabel, self.addButton];
}
Users can increase system font size for readability. Your app must respond correctly.
// Respect system text scaling
Text(
'Product Name',
style: Theme.of(context).textTheme.titleLarge,
// Flutter respects MediaQuery.textScaleFactor by default
)
// Set max scale factor to prevent layout breakage
Text(
'Price: \$99.99',
textScaler: TextScaler.linear(
MediaQuery.textScalerOf(context).clamp(minScaleFactor: 1.0, maxScaleFactor: 2.0),
),
)
// Test with different scale factors
MediaQuery(
data: MediaQuery.of(context).copyWith(textScaler: const TextScaler.linear(2.0)),
child: MyScreen(),
)
// Compose — use sp for text (scales automatically)
Text(
"Product Name",
fontSize = 16.sp, // scales with system font size
)
// XML — use sp for text sizes
// android:textSize="16sp" ← scales with system
// android:textSize="16dp" ← does NOT scale (avoid for body text)
// Test: Settings > Accessibility > Font size > Largest
// SwiftUI — use Dynamic Type (automatic with system fonts)
Text("Product Name")
.font(.title) // scales automatically with Dynamic Type
// Custom font with Dynamic Type scaling
Text("Price")
.font(.custom("MyFont", size: 16, relativeTo: .body))
// UIKit — use preferred fonts
label.font = UIFont.preferredFont(forTextStyle: .body)
label.adjustsFontForContentSizeCategory = true // auto-update on change
// Test: Settings > Accessibility > Display & Text Size > Larger Text
Rules:
| Standard | Minimum Ratio (normal text) | Minimum Ratio (large text) |
|---|---|---|
| WCAG AA | 4.5:1 | 3:1 |
| WCAG AAA | 7:1 | 4.5:1 |
Large text = 18pt (24px) regular or 14pt (18.7px) bold
Common violations:
Tools for checking:
Accessibility Inspector in DevToolsAccessibility Scanner app (Google)Accessibility Inspector in XcodeDon't rely on color alone:
// Bad: only color indicates error
TextField(decoration: InputDecoration(
border: OutlineInputBorder(borderSide: BorderSide(color: Colors.red)),
))
// Good: color + icon + text
TextField(decoration: InputDecoration(
border: OutlineInputBorder(borderSide: BorderSide(color: Colors.red)),
errorText: 'Email is required', // text explanation
suffixIcon: Icon(Icons.error, color: Colors.red, semanticLabel: 'Error'),
))
| Platform | Minimum Size | Recommended |
|---|---|---|
| Android (Material) | 48x48dp | 48x48dp |
| iOS (HIG) | 44x44pt | 44x44pt |
| WCAG 2.2 | 24x24 CSS px | 44x44 CSS px |
| Flutter | 48x48 logical px | 48x48 logical px |
// Flutter — ensure minimum tap target
SizedBox(
width: 48,
height: 48,
child: IconButton(
icon: Icon(Icons.close),
onPressed: onClose,
),
)
// Or use MaterialTapTargetSize
Theme(
data: Theme.of(context).copyWith(
materialTapTargetSize: MaterialTapTargetSize.padded, // ensures 48x48
),
child: Checkbox(value: checked, onChanged: onChanged),
)
// Android Compose — minimum touch target
IconButton(
onClick = { /* ... */ },
modifier = Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp)
) {
Icon(Icons.Default.Close, contentDescription = "Close")
}
// SwiftUI — increase tap area
Button(action: { /* ... */ }) {
Image(systemName: "xmark")
}
.frame(minWidth: 44, minHeight: 44) // meet HIG minimum
Focus order must be logical — typically left-to-right, top-to-bottom, matching visual layout.
// Control focus traversal order
FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(
children: [
FocusTraversalOrder(order: NumericFocusOrder(1), child: TextField(/* name */)),
FocusTraversalOrder(order: NumericFocusOrder(2), child: TextField(/* email */)),
FocusTraversalOrder(order: NumericFocusOrder(3), child: ElevatedButton(/* submit */)),
],
),
)
// Move focus programmatically (after dialog opens, error appears)
FocusScope.of(context).requestFocus(targetFocusNode);
// Compose — custom traversal
Modifier.focusProperties {
next = emailFocusRequester
}
// XML — explicit focus order
// android:nextFocusDown="@id/email_field"
// android:nextFocusForward="@id/email_field"
// Move focus to error field
errorField.requestFocus()
errorField.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
// SwiftUI — manage focus
@FocusState private var focusedField: Field?
enum Field { case name, email, submit }
TextField("Name", text: $name)
.focused($focusedField, equals: .name)
.onSubmit { focusedField = .email }
// Move focus to error
if hasError { focusedField = .name }
// UIKit — accessibility focus
UIAccessibility.post(notification: .layoutChanged, argument: errorLabel)
| Platform | Screen Reader | How to Test |
|---|---|---|
| Android | TalkBack | Settings > Accessibility > TalkBack. Navigate by swiping left/right |
| iOS | VoiceOver | Settings > Accessibility > VoiceOver. Navigate by swiping left/right |
| Flutter | TalkBack (Android) + VoiceOver (iOS) | Test on both platforms, Flutter maps Semantics to native a11y APIs |
Testing checklist with screen reader:
Automated testing tools:
| Tool | Platform | Type |
|---|---|---|
| Accessibility Scanner | Android | On-device automated scan |
| Accessibility Inspector | iOS / macOS | Xcode tool for inspecting a11y tree |
| Flutter DevTools (a11y) | Flutter | Inspect semantics tree, contrast checker |
| Espresso a11y checks | Android | CI-integrated: AccessibilityChecks.enable() |
| XCUITest a11y audit | iOS | try app.performAccessibilityAudit() (Xcode 15+) |
Android — Espresso automated a11y checks:
@Before
fun setUp() {
AccessibilityChecks.enable()
.setRunChecksFromRootView(true)
.setThrowExceptionFor(AccessibilityCheckResult.AccessibilityCheckResultType.ERROR)
}
iOS — XCUITest automated audit:
func testAccessibility() throws {
let app = XCUIApplication()
app.launch()
try app.performAccessibilityAudit()
}
reduceMotion / disableAnimations)## Accessibility Assessment
- **Platform:** [Flutter / Android / iOS]
- **Compliance Target:** [WCAG 2.1 AA / Section 508]
- **Testing Method:** [Manual screen reader / Automated scan / Both]
## Findings
| # | Category | Issue | Severity | Screen/Component | Fix |
|---|----------|-------|----------|-----------------|-----|
| 1 | ... | ... | ... | ... | ... |
## Implementation Plan
[Prioritized fixes by severity and effort]
Semantics widget doesn't always map 1:1 to native a11y APIs — always test with actual TalkBack and VoiceOver on real devicesMediaQuery.disableAnimations (Flutter), UIAccessibility.isReduceMotionEnabled (iOS), Settings.Global.ANIMATOR_DURATION_SCALE (Android)Source: ashutoshsrivastava17/skill-library — distributed by TomeVault.