web-accessibility
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWeb Accessibility
Web可访问性
Build interfaces that work for everyone. These are not optional enhancements — they are baseline quality.
构建人人可用的界面。这些并非可选的增强功能,而是基础质量要求。
Semantic HTML
语义化HTML
Use the right element for the job. Never simulate interactive elements with .
<div>tsx
// BAD: div with click handler
<div onClick={handleClick} className="button">Submit</div>
// GOOD: semantic button
<button onClick={handleClick}>Submit</button>
// BAD: div as link
<div onClick={() => router.push('/about')}>About</div>
// GOOD: anchor/Link for navigation
<Link href="/about">About</Link>为不同场景使用正确的元素。切勿用模拟交互式元素。
<div>tsx
// BAD: div with click handler
<div onClick={handleClick} className="button">Submit</div>
// GOOD: semantic button
<button onClick={handleClick}>Submit</button>
// BAD: div as link
<div onClick={() => router.push('/about')}>About</div>
// GOOD: anchor/Link for navigation
<Link href="/about">About</Link>Element Selection Guide
元素选择指南
| Purpose | Element | Not |
|---|---|---|
| Action (submit, toggle, delete) | | |
| Navigation to URL | | |
| Form input | | Custom div-based inputs |
| Section heading | | |
| List of items | | Repeated |
| Navigation group | | |
| Main content | | |
| 用途 | 元素 | 避免使用 |
|---|---|---|
| 操作(提交、切换、删除) | | |
| 跳转到URL | | |
| 表单输入 | | 基于div的自定义输入框 |
| 章节标题 | | |
| 项目列表 | | 重复的 |
| 导航组 | | |
| 主要内容 | | |
Keyboard Navigation
键盘导航
Every interactive element must be keyboard accessible.
所有交互式元素都必须支持键盘访问。
Focus Management
焦点管理
css
/* NEVER remove focus indicators without replacement */
/* BAD */
*:focus { outline: none; }
/* GOOD: Visible focus only on keyboard navigation */
.interactive:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
/* Group focus for compound controls */
.input-group:focus-within {
outline: 2px solid var(--color-accent);
}css
/* NEVER remove focus indicators without replacement */
/* BAD */
*:focus { outline: none; }
/* GOOD: Visible focus only on keyboard navigation */
.interactive:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
/* Group focus for compound controls */
.input-group:focus-within {
outline: 2px solid var(--color-accent);
}Keyboard Event Handling
键盘事件处理
tsx
// Interactive custom elements need keyboard support
function CustomButton({ onClick, children }: { onClick: () => void; children: React.ReactNode }) {
return (
<div
role="button"
tabIndex={0}
onClick={onClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick();
}
}}
>
{children}
</div>
);
}
// Better: just use <button> and avoid all of the abovetsx
// Interactive custom elements need keyboard support
function CustomButton({ onClick, children }: { onClick: () => void; children: React.ReactNode }) {
return (
<div
role="button"
tabIndex={0}
onClick={onClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick();
}
}}
>
{children}
</div>
);
}
// Better: just use <button> and avoid all of the aboveSkip Links
跳转链接
Provide skip navigation for keyboard users.
tsx
<a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50">
Skip to main content
</a>Headings used as scroll targets need offset for fixed headers:
css
[id] { scroll-margin-top: 5rem; }为键盘用户提供跳过导航的功能。
tsx
<a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50">
Skip to main content
</a>用作滚动目标的标题需要为固定头部设置偏移:
css
[id] { scroll-margin-top: 5rem; }ARIA Patterns
ARIA模式
Icon Buttons
图标按钮
tsx
// Icon-only buttons MUST have aria-label
<button aria-label="Close dialog" onClick={onClose}>
<XIcon aria-hidden="true" />
</button>
// Decorative icons are hidden from screen readers
<span aria-hidden="true">🔒</span> Secure connectiontsx
// Icon-only buttons MUST have aria-label
<button aria-label="Close dialog" onClick={onClose}>
<XIcon aria-hidden="true" />
</button>
// Decorative icons are hidden from screen readers
<span aria-hidden="true">🔒</span> Secure connectionLive Regions
实时区域
Announce dynamic content changes to screen readers.
tsx
// Toast notifications
<div role="status" aria-live="polite">
{notification && <p>{notification.message}</p>}
</div>
// Error alerts
<div role="alert" aria-live="assertive">
{error && <p>{error.message}</p>}
</div>向屏幕阅读器播报动态内容的变化。
tsx
// Toast notifications
<div role="status" aria-live="polite">
{notification && <p>{notification.message}</p>}
</div>
// Error alerts
<div role="alert" aria-live="assertive">
{error && <p>{error.message}</p>}
</div>Loading States
加载状态
tsx
<button disabled={isLoading} aria-busy={isLoading}>
{isLoading ? 'Saving\u2026' : 'Save'} {/* proper ellipsis character */}
</button>
// Skeleton screens
<div aria-busy="true" aria-label="Loading content">
<Skeleton />
</div>tsx
<button disabled={isLoading} aria-busy={isLoading}>
{isLoading ? 'Saving\u2026' : 'Save'} {/* proper ellipsis character */}
</button>
// Skeleton screens
<div aria-busy="true" aria-label="Loading content">
<Skeleton />
</div>Forms
表单
Labels
标签
Every input must have an associated label.
tsx
// GOOD: Explicit association
<label htmlFor="email">Email</label>
<input id="email" type="email" autoComplete="email" />
// GOOD: Wrapping (clickable label, no htmlFor needed)
<label>
Email
<input type="email" autoComplete="email" />
</label>
// GOOD: Visually hidden but accessible
<label htmlFor="search" className="sr-only">Search</label>
<input id="search" type="search" placeholder="Search..." />每个输入框都必须关联对应的标签。
tsx
// GOOD: Explicit association
<label htmlFor="email">Email</label>
<input id="email" type="email" autoComplete="email" />
// GOOD: Wrapping (clickable label, no htmlFor needed)
<label>
Email
<input type="email" autoComplete="email" />
</label>
// GOOD: Visually hidden but accessible
<label htmlFor="search" className="sr-only">Search</label>
<input id="search" type="search" placeholder="Search..." />Input Types and Autocomplete
输入类型与自动补全
Use semantic input types to get the right mobile keyboard and browser behavior.
tsx
<input type="email" autoComplete="email" />
<input type="tel" autoComplete="tel" />
<input type="url" autoComplete="url" />
<input type="password" autoComplete="current-password" />
<input type="password" autoComplete="new-password" />使用语义化输入类型以获得适配的移动端键盘和浏览器行为。
tsx
<input type="email" autoComplete="email" />
<input type="tel" autoComplete="tel" />
<input type="url" autoComplete="url" />
<input type="password" autoComplete="current-password" />
<input type="password" autoComplete="new-password" />Validation and Errors
验证与错误提示
tsx
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
<p id="email-error" role="alert" className="text-red-600">
{errors.email}
</p>
)}
</div>tsx
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
<p id="email-error" role="alert" className="text-red-600">
{errors.email}
</p>
)}
</div>Form Behavior Rules
表单行为规则
- Never prevent paste on any input
- Disable spellcheck on emails and codes:
spellCheck={false} - Submit button stays enabled until request starts; show spinner during loading
- Focus first error on submit failure
- Checkboxes and radio buttons: single hit target, no dead zones between label and input
- 切勿禁止粘贴任何输入框的内容
- 关闭拼写检查:邮箱和验证码输入框设置
spellCheck={false} - 提交按钮保持启用状态直到请求开始;加载期间显示加载动画
- 提交失败时聚焦第一个错误输入框
- 复选框和单选按钮:单个点击目标,标签与输入框之间无无效区域
Images and Media
图片与媒体
tsx
// Informative images: descriptive alt
<img src="chart.png" alt="Revenue grew 40% from Q1 to Q3 2025" />
// Decorative images: empty alt
<img src="divider.svg" alt="" />
// Prevent layout shift: always set dimensions
<img src="photo.jpg" width={800} height={600} alt="Team photo" />
// Below fold: lazy load
<img src="photo.jpg" loading="lazy" alt="..." />
// Critical: prioritize
<img src="hero.jpg" fetchPriority="high" alt="..." />tsx
// Informative images: descriptive alt
<img src="chart.png" alt="Revenue grew 40% from Q1 to Q3 2025" />
// Decorative images: empty alt
<img src="divider.svg" alt="" />
// Prevent layout shift: always set dimensions
<img src="photo.jpg" width={800} height={600} alt="Team photo" />
// Below fold: lazy load
<img src="photo.jpg" loading="lazy" alt="..." />
// Critical: prioritize
<img src="hero.jpg" fetchPriority="high" alt="..." />Touch and Mobile
触摸与移动端适配
Touch Targets
触摸目标
Minimum 44x44px for all interactive elements (WCAG 2.5.5).
css
.touch-target {
min-height: 44px;
min-width: 44px;
}所有交互式元素的触摸目标最小尺寸为44x44px(符合WCAG 2.5.5标准)。
css
.touch-target {
min-height: 44px;
min-width: 44px;
}Safe Areas
安全区域适配
Handle device notches and home indicators.
css
.full-bleed {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}适配设备刘海和底部指示条。
css
.full-bleed {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}Touch Behavior
触摸行为优化
css
/* Prevent 300ms delay and highlight flash */
.interactive {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
/* Prevent scroll chaining on modals */
.modal { overscroll-behavior: contain; }css
/* Prevent 300ms delay and highlight flash */
.interactive {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
/* Prevent scroll chaining on modals */
.modal { overscroll-behavior: contain; }Internationalization
国际化
tsx
// BAD: Hardcoded formats
const date = `${month}/${day}/${year}`;
const price = `$${amount.toFixed(2)}`;
// GOOD: Locale-aware formatting
const date = new Intl.DateTimeFormat(locale).format(new Date());
const price = new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD',
}).format(amount);
// Detect language
const lang = request.headers.get('accept-language')?.split(',')[0] ?? 'en';tsx
// BAD: Hardcoded formats
const date = `${month}/${day}/${year}`;
const price = `$${amount.toFixed(2)}`;
// GOOD: Locale-aware formatting
const date = new Intl.DateTimeFormat(locale).format(new Date());
const price = new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD',
}).format(amount);
// Detect language
const lang = request.headers.get('accept-language')?.split(',')[0] ?? 'en';Performance for Accessibility
可访问性相关性能优化
- Lists > 50 items: virtualize
- Critical fonts: with
<link rel="preload" as="font">font-display: swap - Avoid layout reads during render (causes jank for screen reader users too)
- Uncontrolled inputs perform better than controlled for large forms
- 超过50条数据的列表:使用虚拟滚动
- 关键字体:使用并设置
<link rel="preload" as="font">font-display: swap - 避免在渲染期间读取布局(会导致界面卡顿,影响屏幕阅读器用户)
- 大型表单使用非受控输入框比受控输入框性能更好
Checklist
检查清单
Use this for review:
- All interactive elements are keyboard accessible
- Focus indicators are visible on
:focus-visible - Color contrast meets WCAG AA (4.5:1 body, 3:1 large text)
- Images have appropriate alt text
- Form inputs have labels
- Error messages are associated with inputs via
aria-describedby - Icon-only buttons have
aria-label - Dynamic content uses regions
aria-live - Touch targets are minimum 44x44px
- is respected
prefers-reduced-motion - No without replacement focus style
outline: none - Heading hierarchy is sequential (no skipped levels)
用于审查时使用:
- 所有交互式元素支持键盘访问
- 焦点指示器在状态下可见
:focus-visible - 颜色对比度符合WCAG AA标准(正文4.5:1,大文本3:1)
- 图片配有合适的替代文本
- 表单输入框都有关联标签
- 错误提示通过与输入框关联
aria-describedby - 仅含图标的按钮设置了
aria-label - 动态内容使用区域
aria-live - 触摸目标最小尺寸为44x44px
- 尊重设置
prefers-reduced-motion - 没有在未替换焦点样式的情况下使用
outline: none - 标题层级连续(无层级跳跃)