framework-accessibility

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Framework-Specific Accessibility Patterns

框架特定无障碍模式

React / Next.js

React / Next.js

Common Pitfalls

常见陷阱

PatternIssueFix
onClick
on
<div>
Not keyboard accessibleUse
<button>
or add
role="button"
,
tabIndex={0}
,
onKeyDown
dangerouslySetInnerHTML
May inject inaccessible contentAudit injected HTML for ARIA, headings, alt text
React.Fragment
as root
May break landmark treeEnsure fragments don't interrupt landmark nesting
Missing
key
on lists
Can cause focus loss on re-renderUse stable keys (not array index) for interactive lists
Portal without focus trapFocus can escape to backgroundWrap portal content in
FocusTrap
component
useEffect
focus management
Focus may not fire on mountUse
useRef
+
useEffect
with proper dependency array
模式问题修复方案
<div>
上使用
onClick
不支持键盘访问使用
<button>
或添加
role="button"
tabIndex={0}
onKeyDown
使用
dangerouslySetInnerHTML
可能注入无障碍属性缺失的内容审计注入的HTML是否包含正确的ARIA属性、标题层级、替代文本
根节点使用
React.Fragment
可能破坏地标树结构确保Fragment不会中断地标的嵌套关系
列表项缺失
key
属性
重新渲染时可能导致焦点丢失为可交互列表使用稳定的key(不要用数组索引)
Portal未添加焦点捕获焦点可能逸出到背景内容将Portal内容包裹在
FocusTrap
组件中
useEffect
焦点管理逻辑问题
组件挂载时焦点可能不触发搭配使用
useRef
+ 依赖数组配置正确的
useEffect

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

常见陷阱

PatternIssueFix
v-html
with user content
May inject inaccessible markupSanitize and audit injected HTML
v-if
on live regions
Removes element from DOM, breaks announcementsUse
v-show
for live regions instead
<transition>
without focus
Focus lost when content transitionsManage focus in
@after-enter
hook
<teleport>
to body
Content outside app landmark treeAdd landmark roles to teleported content
模式问题修复方案
对用户生成内容使用
v-html
可能注入无障碍属性缺失的标记对注入的HTML进行消毒和无障碍审计
对实时区域使用
v-if
会将元素从DOM中移除,破坏屏幕阅读器播报对实时区域改用
v-show
<transition>
未处理焦点
内容过渡时焦点丢失
@after-enter
钩子中管理焦点
使用
<teleport>
挂载到body
内容位于应用地标树外部为传送的内容添加地标角色

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

常见陷阱

PatternIssueFix
[aria-label]
binding
Invalid - ARIA is not a propertyUse
[attr.aria-label]
*ngFor
without
trackBy
Focus loss on list re-renderAdd
trackBy
function
No
LiveAnnouncer
Route changes not announcedInject
LiveAnnouncer
and announce navigation
OnPush
+ live regions
Change detection may not triggerUse
ChangeDetectorRef.markForCheck()
模式问题修复方案
使用
[aria-label]
绑定
无效 - ARIA不是元素属性使用
[attr.aria-label]
*ngFor
未搭配
trackBy
列表重新渲染时焦点丢失添加
trackBy
函数
未使用
LiveAnnouncer
路由变更不会被屏幕阅读器播报注入
LiveAnnouncer
并播报导航事件
OnPush
变更检测策略搭配实时区域
可能无法触发变更检测使用
ChangeDetectorRef.markForCheck()

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

常见陷阱

PatternIssueFix
{#if}
without focus management
Focus lost when content appearsUse
use:action
to focus new content
transition:
without motion check
Animations play regardless of user preferenceAdd
prefers-reduced-motion
check
on:click
on non-interactive
Not keyboard accessibleUse
<button>
or add keyboard handlers
模式问题修复方案
使用
{#if}
时未做焦点管理
内容出现时焦点丢失使用
use:action
为新出现的内容设置焦点
使用
transition:
时未做动效偏好检查
无论用户偏好如何都会播放动画添加
prefers-reduced-motion
检查
非交互元素绑定
on:click
不支持键盘访问使用
<button>
或添加键盘事件处理逻辑

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

常见陷阱

PatternIssueFix
outline-none
Removes focus indicatorPair with
ring-2 ring-offset-2 focus-visible:ring-blue-500
text-gray-400
on
bg-white
Fails 4.5:1 contrastUse
text-gray-600
or darker
No
motion-reduce:
variant
Animations ignore user preferenceAdd
motion-reduce:transition-none
Missing
focus:
styles
No visible focus indicatorAdd
focus:ring-2 focus:ring-blue-500
sr-only
missing
Screen reader text not availableAdd
<span class="sr-only">description</span>
模式问题修复方案
使用
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">描述内容</span>

Contrast-Safe Tailwind Pairs

符合对比度要求的Tailwind配色配对

BackgroundMinimum TextRatio
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
背景色最低要求文本色对比度
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

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>