capacitor-accessibility
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAccessibility in Capacitor Apps
Capacitor应用中的无障碍设计
Build inclusive apps that work for everyone.
打造面向所有用户的包容性应用。
When to Use This Skill
何时使用本技能
- User needs accessibility
- User wants screen reader support
- User asks about WCAG
- User needs focus management
- 用户需要提升应用无障碍性
- 用户希望支持屏幕阅读器
- 用户咨询WCAG相关问题
- 用户需要焦点管理方案
Quick Checklist
快速检查清单
- Semantic HTML
- Alt text for images
- Touch targets 44x44pt
- Color contrast 4.5:1
- Focus indicators
- Screen reader labels
- Keyboard navigation
- 语义化HTML
- 图片替代文本
- 触控目标尺寸44x44pt
- 色彩对比度4.5:1
- 焦点指示器
- 屏幕阅读器标签
- 键盘导航
Screen Reader Support
屏幕阅读器支持
Labels and Hints
标签与提示
tsx
// Accessible button
<button
aria-label="Delete item"
aria-describedby="delete-hint"
>
<TrashIcon />
</button>
<span id="delete-hint" className="sr-only">
Permanently removes this item
</span>
// Accessible input
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-required="true"
aria-invalid={hasError}
aria-describedby={hasError ? "email-error" : undefined}
/>
{hasError && <span id="email-error">Invalid email</span>}tsx
// Accessible button
<button
aria-label="Delete item"
aria-describedby="delete-hint"
>
<TrashIcon />
</button>
<span id="delete-hint" className="sr-only">
Permanently removes this item
</span>
// Accessible input
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-required="true"
aria-invalid={hasError}
aria-describedby={hasError ? "email-error" : undefined}
/>
{hasError && <span id="email-error">Invalid email</span>}Live Regions
实时区域
tsx
// Announce dynamic content
<div aria-live="polite" aria-atomic="true">
{message}
</div>
// Urgent announcements
<div aria-live="assertive" role="alert">
{error}
</div>tsx
// Announce dynamic content
<div aria-live="polite" aria-atomic="true">
{message}
</div>
// Urgent announcements
<div aria-live="assertive" role="alert">
{error}
</div>Touch Targets
触控目标
css
/* Minimum 44x44pt */
button, a, input {
min-height: 44px;
min-width: 44px;
}
/* Icon buttons need padding */
.icon-button {
padding: 12px;
}css
/* Minimum 44x44pt */
button, a, input {
min-height: 44px;
min-width: 44px;
}
/* Icon buttons need padding */
.icon-button {
padding: 12px;
}Color Contrast
色彩对比度
css
/* Good contrast (4.5:1 for text) */
.text {
color: #333333;
background: #ffffff;
}
/* Don't rely on color alone */
.error {
color: #d32f2f;
border-left: 4px solid #d32f2f; /* Visual indicator */
}
.error::before {
content: "⚠ "; /* Icon indicator */
}css
/* Good contrast (4.5:1 for text) */
.text {
color: #333333;
background: #ffffff;
}
/* Don't rely on color alone */
.error {
color: #d32f2f;
border-left: 4px solid #d32f2f; /* Visual indicator */
}
.error::before {
content: "⚠ "; /* Icon indicator */
}Focus Management
焦点管理
typescript
// Move focus after navigation
useEffect(() => {
const heading = document.querySelector('h1');
heading?.focus();
}, [page]);
// Trap focus in modals
function trapFocus(element: HTMLElement) {
const focusable = element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0] as HTMLElement;
const last = focusable[focusable.length - 1] as HTMLElement;
element.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
});
}typescript
// Move focus after navigation
useEffect(() => {
const heading = document.querySelector('h1');
heading?.focus();
}, [page]);
// Trap focus in modals
function trapFocus(element: HTMLElement) {
const focusable = element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0] as HTMLElement;
const last = focusable[focusable.length - 1] as HTMLElement;
element.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
});
}Native Accessibility
原生无障碍支持
iOS VoiceOver
iOS VoiceOver
swift
// Custom accessibility in native code
element.isAccessibilityElement = true
element.accessibilityLabel = "Play video"
element.accessibilityHint = "Double tap to play"
element.accessibilityTraits = .buttonswift
// Custom accessibility in native code
element.isAccessibilityElement = true
element.accessibilityLabel = "Play video"
element.accessibilityHint = "Double tap to play"
element.accessibilityTraits = .buttonAndroid TalkBack
Android TalkBack
kotlin
// Custom accessibility
ViewCompat.setAccessibilityDelegate(view, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(
host: View,
info: AccessibilityNodeInfoCompat
) {
super.onInitializeAccessibilityNodeInfo(host, info)
info.contentDescription = "Play video"
}
})kotlin
// Custom accessibility
ViewCompat.setAccessibilityDelegate(view, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(
host: View,
info: AccessibilityNodeInfoCompat
) {
super.onInitializeAccessibilityNodeInfo(host, info)
info.contentDescription = "Play video"
}
})Testing
测试
bash
undefinedbash
undefinediOS: Enable VoiceOver in Simulator
iOS: 在模拟器中启用VoiceOver
Settings > Accessibility > VoiceOver
设置 > 辅助功能 > VoiceOver
Android: Enable TalkBack
Android: 启用TalkBack
Settings > Accessibility > TalkBack
设置 > 辅助功能 > TalkBack
Web: Use axe-core
Web: 使用axe-core
bunx @axe-core/cli https://localhost:3000
undefinedbunx @axe-core/cli https://localhost:3000
undefinedResources
相关资源
- WCAG Guidelines: https://www.w3.org/WAI/WCAG21/quickref
- iOS Accessibility: https://developer.apple.com/accessibility
- Android Accessibility: https://developer.android.com/accessibility
- WCAG指南: https://www.w3.org/WAI/WCAG21/quickref
- iOS无障碍开发: https://developer.apple.com/accessibility
- Android无障碍开发: https://developer.android.com/accessibility