framework-accessibility
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFramework-Specific Accessibility Patterns
框架特定无障碍模式
React / Next.js
React / Next.js
Common Pitfalls
常见陷阱
| Pattern | Issue | Fix |
|---|---|---|
| Not keyboard accessible | Use |
| May inject inaccessible content | Audit injected HTML for ARIA, headings, alt text |
| May break landmark tree | Ensure fragments don't interrupt landmark nesting |
Missing | Can cause focus loss on re-render | Use stable keys (not array index) for interactive lists |
| Portal without focus trap | Focus can escape to background | Wrap portal content in |
| Focus may not fire on mount | Use |
| 模式 | 问题 | 修复方案 |
|---|---|---|
| 不支持键盘访问 | 使用 |
使用 | 可能注入无障碍属性缺失的内容 | 审计注入的HTML是否包含正确的ARIA属性、标题层级、替代文本 |
根节点使用 | 可能破坏地标树结构 | 确保Fragment不会中断地标的嵌套关系 |
列表项缺失 | 重新渲染时可能导致焦点丢失 | 为可交互列表使用稳定的key(不要用数组索引) |
| Portal未添加焦点捕获 | 焦点可能逸出到背景内容 | 将Portal内容包裹在 |
| 组件挂载时焦点可能不触发 | 搭配使用 |
Fix Templates
修复模板
jsx
// Bad: div as button
<div onClick={handleClick}>Submit</div>
// Good: semantic button
<button onClick={handleClick}>Submit</button>
// Bad: image without alt in Next.js
<Image src="/hero.jpg" width={800} height={400} />
// Good: image with alt
<Image src="/hero.jpg" width={800} height={400} alt="Team collaborating in a modern office" />
// Bad: no focus management on route change
useEffect(() => {
// nothing
}, [location]);
// Good: focus management on route change
useEffect(() => {
const mainContent = document.getElementById('main-content');
if (mainContent) {
mainContent.focus();
mainContent.scrollIntoView();
}
}, [location]);
// Bad: link opening new tab
<a href={url} target="_blank">Resource</a>
// Good: link with new tab warning
<a href={url} target="_blank" rel="noopener noreferrer">
Resource <span className="sr-only">(opens in new tab)</span>
</a>jsx
// Bad: div as button
<div onClick={handleClick}>Submit</div>
// Good: semantic button
<button onClick={handleClick}>Submit</button>
// Bad: image without alt in Next.js
<Image src="/hero.jpg" width={800} height={400} />
// Good: image with alt
<Image src="/hero.jpg" width={800} height={400} alt="Team collaborating in a modern office" />
// Bad: no focus management on route change
useEffect(() => {
// nothing
}, [location]);
// Good: focus management on route change
useEffect(() => {
const mainContent = document.getElementById('main-content');
if (mainContent) {
mainContent.focus();
mainContent.scrollIntoView();
}
}, [location]);
// Bad: link opening new tab
<a href={url} target="_blank">Resource</a>
// Good: link with new tab warning
<a href={url} target="_blank" rel="noopener noreferrer">
Resource <span className="sr-only">(opens in new tab)</span>
</a>Vue
Vue
Common Pitfalls
常见陷阱
| Pattern | Issue | Fix |
|---|---|---|
| May inject inaccessible markup | Sanitize and audit injected HTML |
| Removes element from DOM, breaks announcements | Use |
| Focus lost when content transitions | Manage focus in |
| Content outside app landmark tree | Add landmark roles to teleported content |
| 模式 | 问题 | 修复方案 |
|---|---|---|
对用户生成内容使用 | 可能注入无障碍属性缺失的标记 | 对注入的HTML进行消毒和无障碍审计 |
对实时区域使用 | 会将元素从DOM中移除,破坏屏幕阅读器播报 | 对实时区域改用 |
| 内容过渡时焦点丢失 | 在 |
使用 | 内容位于应用地标树外部 | 为传送的内容添加地标角色 |
Fix Templates
修复模板
vue
<!-- Bad: v-if on live region -->
<div v-if="message" aria-live="polite">{{ message }}</div>
<!-- Good: v-show keeps element in DOM -->
<div v-show="message" aria-live="polite">{{ message }}</div>
<!-- Bad: no focus after transition -->
<transition name="fade">
<div v-if="showModal" class="modal">...</div>
</transition>
<!-- Good: focus managed after transition -->
<transition name="fade" @after-enter="focusModal">
<div v-if="showModal" ref="modal" class="modal" tabindex="-1">...</div>
</transition>vue
<!-- Bad: v-if on live region -->
<div v-if="message" aria-live="polite">{{ message }}</div>
<!-- Good: v-show keeps element in DOM -->
<div v-show="message" aria-live="polite">{{ message }}</div>
<!-- Bad: no focus after transition -->
<transition name="fade">
<div v-if="showModal" class="modal">...</div>
</transition>
<!-- Good: focus managed after transition -->
<transition name="fade" @after-enter="focusModal">
<div v-if="showModal" ref="modal" class="modal" tabindex="-1">...</div>
</transition>Angular
Angular
Common Pitfalls
常见陷阱
| Pattern | Issue | Fix |
|---|---|---|
| Invalid - ARIA is not a property | Use |
| Focus loss on list re-render | Add |
No | Route changes not announced | Inject |
| Change detection may not trigger | Use |
| 模式 | 问题 | 修复方案 |
|---|---|---|
使用 | 无效 - ARIA不是元素属性 | 使用 |
| 列表重新渲染时焦点丢失 | 添加 |
未使用 | 路由变更不会被屏幕阅读器播报 | 注入 |
| 可能无法触发变更检测 | 使用 |
Fix Templates
修复模板
typescript
// Bad: ARIA binding
<button [aria-label]="label">X</button>
// Good: ARIA attribute binding
<button [attr.aria-label]="label">X</button>
// Bad: ngFor without trackBy
<li *ngFor="let item of items">{{ item.name }}</li>
// Good: ngFor with trackBy
<li *ngFor="let item of items; trackBy: trackById">{{ item.name }}</li>
// Route change announcements
constructor(private liveAnnouncer: LiveAnnouncer, private router: Router) {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe((event: NavigationEnd) => {
this.liveAnnouncer.announce(`Navigated to ${this.getPageTitle()}`);
});
}typescript
// Bad: ARIA binding
<button [aria-label]="label">X</button>
// Good: ARIA attribute binding
<button [attr.aria-label]="label">X</button>
// Bad: ngFor without trackBy
<li *ngFor="let item of items">{{ item.name }}</li>
// Good: ngFor with trackBy
<li *ngFor="let item of items; trackBy: trackById">{{ item.name }}</li>
// Route change announcements
constructor(private liveAnnouncer: LiveAnnouncer, private router: Router) {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe((event: NavigationEnd) => {
this.liveAnnouncer.announce(`Navigated to ${this.getPageTitle()}`);
});
}Svelte
Svelte
Common Pitfalls
常见陷阱
| Pattern | Issue | Fix |
|---|---|---|
| Focus lost when content appears | Use |
| Animations play regardless of user preference | Add |
| Not keyboard accessible | Use |
| 模式 | 问题 | 修复方案 |
|---|---|---|
使用 | 内容出现时焦点丢失 | 使用 |
使用 | 无论用户偏好如何都会播放动画 | 添加 |
非交互元素绑定 | 不支持键盘访问 | 使用 |
Fix Templates
修复模板
svelte
<!-- Bad: click on div -->
<div on:click={toggle}>Toggle</div>
<!-- Good: keyboard accessible -->
<button on:click={toggle}>Toggle</button>
<!-- Bad: animation without motion preference -->
<div transition:fly={{ y: 200 }}>Content</div>
<!-- Good: respects motion preference -->
<div transition:fly={{ y: reducedMotion ? 0 : 200, duration: reducedMotion ? 0 : 300 }}>Content</div>
<script>
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
</script>svelte
<!-- Bad: click on div -->
<div on:click={toggle}>Toggle</div>
<!-- Good: keyboard accessible -->
<button on:click={toggle}>Toggle</button>
<!-- Bad: animation without motion preference -->
<div transition:fly={{ y: 200 }}>Content</div>
<!-- Good: respects motion preference -->
<div transition:fly={{ y: reducedMotion ? 0 : 200, duration: reducedMotion ? 0 : 300 }}>Content</div>
<script>
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
</script>Tailwind CSS
Tailwind CSS
Common Pitfalls
常见陷阱
| Pattern | Issue | Fix |
|---|---|---|
| Removes focus indicator | Pair with |
| Fails 4.5:1 contrast | Use |
No | Animations ignore user preference | Add |
Missing | No visible focus indicator | Add |
| Screen reader text not available | Add |
| 模式 | 问题 | 修复方案 |
|---|---|---|
使用 | 移除了焦点指示器 | 搭配使用 |
| 不满足4.5:1的对比度要求 | 使用 |
未使用 | 动效忽略用户的动效偏好设置 | 添加 |
缺失 | 没有可见的焦点指示器 | 添加 |
缺失 | 屏幕阅读器无法获取对应文本描述 | 添加 |
Contrast-Safe Tailwind Pairs
符合对比度要求的Tailwind配色配对
| Background | Minimum Text | Ratio |
|---|---|---|
| | 4.55:1 |
| | 6.62:1 |
| | 6.29:1 |
| | 5.92:1 |
| | 5.23:1 |
| | 4.54:1 |
| | 4.58:1 |
| 背景色 | 最低要求文本色 | 对比度 |
|---|---|---|
| | 4.55:1 |
| | 6.62:1 |
| | 6.29:1 |
| | 5.92:1 |
| | 5.23:1 |
| | 4.54:1 |
| | 4.58:1 |
Fix Templates
修复模板
html
<!-- Bad: no focus indicator -->
<button class="bg-blue-500 text-white px-4 py-2 rounded outline-none">
Submit
</button>
<!-- Good: visible focus indicator -->
<button class="bg-blue-500 text-white px-4 py-2 rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2">
Submit
</button>
<!-- Bad: low contrast -->
<p class="text-gray-400">Important information</p>
<!-- Good: adequate contrast -->
<p class="text-gray-700">Important information</p>
<!-- Bad: animation without motion preference -->
<div class="transition-transform duration-300 hover:scale-105">Card</div>
<!-- Good: respects motion preference -->
<div class="transition-transform duration-300 hover:scale-105 motion-reduce:transition-none motion-reduce:hover:scale-100">Card</div>html
<!-- Bad: no focus indicator -->
<button class="bg-blue-500 text-white px-4 py-2 rounded outline-none">
Submit
</button>
<!-- Good: visible focus indicator -->
<button class="bg-blue-500 text-white px-4 py-2 rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2">
Submit
</button>
<!-- Bad: low contrast -->
<p class="text-gray-400">Important information</p>
<!-- Good: adequate contrast -->
<p class="text-gray-700">Important information</p>
<!-- Bad: animation without motion preference -->
<div class="transition-transform duration-300 hover:scale-105">Card</div>
<!-- Good: respects motion preference -->
<div class="transition-transform duration-300 hover:scale-105 motion-reduce:transition-none motion-reduce:hover:scale-100">Card</div>