📖 核心概念
- 无障碍的重要性与法律要求
- ARIA角色与属性
- 键盘导航与焦点管理
- 屏幕阅读器兼容
- 颜色对比度与文字大小
- 无障碍测试工具
💻 代码实现
无障碍组件实现
✅ tsx
// 无障碍模态框
import { useEffect, useRef } from 'react';
function Modal({ isOpen, onClose, title, children }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
// 焦点管理:打开时记住之前的焦点,关闭时恢复
useEffect(() => {
if (isOpen) {
previousFocusRef.current = document.activeElement as HTMLElement;
// 将焦点移到模态框内第一个可聚焦元素
const firstFocusable = modalRef.current?.querySelector<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
firstFocusable?.focus();
} else {
previousFocusRef.current?.focus();
}
}, [isOpen]);
// 焦点陷阱:Tab键不会离开模态框
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') { onClose(); return; }
if (e.key !== 'Tab') return;
const focusable = modalRef.current?.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (!focusable?.length) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
ref={modalRef}
className="modal-overlay"
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
>
<div className="modal-content">
<h2 id="modal-title">{title}</h2>
{children}
<button onClick={onClose} aria-label="关闭对话框">✕</button>
</div>
</div>
);
}
ARIA模式
✅ tsx
// 无障碍手风琴
function Accordion({ items }: { items: { title: string; content: string }[] }) {
const [openIndex, setOpenIndex] = useState<number | null>(null);
return (
<div className="accordion">
{items.map((item, index) => (
<div key={index} className="accordion-item">
<h3>
<button
aria-expanded={openIndex === index}
aria-controls={\`panel-\${index}\`}
id={\`trigger-\${index}\`}
onClick={() => setOpenIndex(openIndex === index ? null : index)}
>
{item.title}
<span aria-hidden="true">{openIndex === index ? '−' : '+'}</span>
</button>
</h3>
<div
id={\`panel-\${index}\`}
role="region"
aria-labelledby={\`trigger-\${index}\`}
hidden={openIndex !== index}
>
<p>{item.content}</p>
</div>
</div>
))}
</div>
);
}
// 活动区域 (aria-live)
function NotificationToast() {
const [messages, setMessages] = useState<string[]>([]);
return (
<div>
{/* aria-live="polite": 屏幕阅读器在空闲时播报 */}
<div aria-live="polite" aria-atomic="true" className="sr-only">
{messages[messages.length - 1]}
</div>
{/* aria-live="assertive": 立即打断播报(错误提示等) */}
{/* <div aria-live="assertive">...</div> */}
</div>
);
}
// 跳过导航链接
function SkipNav() {
return (
<a href="#main-content" className="skip-link">
跳到主要内容
</a>
);
}
/* CSS */
/* .skip-link {
/* position: absolute; left: -9999px; top: 0;
/* background: #06b6d4; color: white; padding: 8px 16px; z-index: 100;
/* }
/* .skip-link:focus { left: 0; }
/*
/* .sr-only {
/* position: absolute; width: 1px; height: 1px;
/* padding: 0; margin: -1px; overflow: hidden;
/* clip: rect(0, 0, 0, 0); border: 0;
/* }
/* */
// 无障碍检查清单
const a11yChecklist = [
'✅ 所有图片有alt文本',
'✅ 表单有label关联',
'✅ 颜色对比度 ≥ 4.5:1',
'✅ 键盘可完全操作',
'✅ 焦点顺序符合逻辑',
'✅ ARIA角色正确使用',
'✅ 跳过导航链接',
'✅ aria-live通知动态内容',
'✅ 文字可缩放至200%',
'✅ 触摸目标 ≥ 44×44px',
];
🎯 练习任务
- 1 实现一个无障碍的模态框,包含焦点陷阱和ESC关闭
- 2 为手风琴组件添加完整的ARIA属性
- 3 使用axe或Lighthouse审计你的页面无障碍性
- 4 用键盘测试你整个应用的导航流程
🏆 成就解锁
无障碍倡导者 — 掌握Web无障碍开发,让每个用户都能使用你的产品