特定框架的无障碍模式
React / Next.js
常见陷阱
| 模式 | 问题 | 修复方法 |
|---|
在 <div> 上使用 onClick | 无法通过键盘访问 | 使用 <button>,或添加 role="button"、tabIndex={0}、onKeyDown |
dangerouslySetInnerHTML | 可能注入无障碍性不足的内容 | 审核注入的 HTML,检查 ARIA、标题和替代文本 |
将 React.Fragment 作为根节点 | 可能破坏地标树 | 确保片段不会打断地标嵌套 |
列表缺少 key | 重新渲染时可能导致焦点丢失 | 对交互式列表使用稳定的键(不要使用数组索引) |
| Portal 未设置焦点陷阱 | 焦点可能移至背景内容 | 使用 FocusTrap 组件包裹 Portal 内容 |
使用 useEffect 管理焦点 | 挂载时焦点可能不会触发 | 使用 useRef + useEffect,并设置正确的依赖数组 |
修复模板
// 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
常见陷阱
| 模式 | 问题 | 修复方法 |
|---|
对用户内容使用 v-html | 可能注入无障碍性不足的标记 | 清理并审核注入的 HTML |
对实时区域使用 v-if | 会从 DOM 中移除元素,导致播报失效 | 对实时区域改用 v-show |
<transition> 未管理焦点 | 内容过渡时焦点丢失 | 在 @after-enter 钩子中管理焦点 |
将 <teleport> 指向 body | 内容位于应用地标树之外 | 为传送的内容添加地标角色 |
修复模板
<div v-if="message" aria-live="polite">{{ message }}</div>
<div v-show="message" aria-live="polite">{{ message }}</div>
<transition name="fade">
<div v-if="showModal" class="modal">...</div>
</transition>
<transition name="fade" @after-enter="focusModal">
<div v-if="showModal" ref="modal" class="modal" tabindex="-1">...</div>
</transition>
Angular
常见陷阱
| 模式 | 问题 | 修复方法 |
|---|
绑定 [aria-label] | 无效——ARIA 不是属性 | 使用 [attr.aria-label] |
*ngFor 未使用 trackBy | 列表重新渲染时焦点丢失 | 添加 trackBy 函数 |
未使用 LiveAnnouncer | 路由更改不会播报 | 注入 LiveAnnouncer 并播报导航信息 |
OnPush + 实时区域 | 变更检测可能不会触发 | 使用 ChangeDetectorRef.markForCheck() |
修复模板
// 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
常见陷阱
| 模式 | 问题 | 修复方法 |
|---|
{#if} 未管理焦点 | 内容出现时焦点丢失 | 使用 use:action 将焦点移至新内容 |
transition: 未检查动画偏好 | 无论用户偏好如何,动画都会播放 | 添加 prefers-reduced-motion 检查 |
在非交互元素上使用 on:click | 无法通过键盘访问 | 使用 <button> 或添加键盘处理器 |
修复模板
<div on:click={toggle}>Toggle</div>
<button on:click={toggle}>Toggle</button>
<div transition:fly={{ y: 200 }}>Content</div>
<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
常见陷阱
| 模式 | 问题 | 修复方法 |
|---|
outline-none | 移除焦点指示器 | 搭配使用 ring-2 ring-offset-2 focus-visible:ring-blue-500 |
在 bg-white 上使用 text-gray-400 | 对比度未达到 4.5:1 | 使用 text-gray-600 或更深的颜色 |
缺少 motion-reduce: 变体 | 动画忽略用户偏好 | 添加 motion-reduce:transition-none |
缺少 focus: 样式 | 没有可见的焦点指示器 | 添加 focus:ring-2 focus:ring-blue-500 |
缺少 sr-only | 屏幕阅读器无法获取文本 | 添加 <span class="sr-only">description</span> |
确保对比度的 Tailwind 配对
| 背景 | 最低文本颜色 | 比率 |
|---|
bg-white | text-gray-600 | 4.55:1 |
bg-white | text-gray-700 | 6.62:1 |
bg-gray-50 | text-gray-700 | 6.29:1 |
bg-gray-900 | text-gray-300 | 5.92:1 |
bg-blue-600 | text-white | 5.23:1 |
bg-red-600 | text-white | 4.54:1 |
bg-green-700 | text-white | 4.58:1 |
修复模板
<button class="bg-blue-500 text-white px-4 py-2 rounded outline-none">
Submit
</button>
<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>
<p class="text-gray-400">Important information</p>
<p class="text-gray-700">Important information</p>
<div class="transition-transform duration-300 hover:scale-105">Card</div>
<div class="transition-transform duration-300 hover:scale-105 motion-reduce:transition-none motion-reduce:hover:scale-100">Card</div>