ARIA 模式
对语义 HTML 不能表达的自定义 widget 正确用 ARIA role、状态和属性。
为什么
语义 HTML 覆盖大多需求(button、nav、main、form、table),但自定义 widget——标签、手风琴、组合框、树视图、菜单——无原生等效。ARIA(无障碍富互联网应用)填此缺加 role(tab、tablist、tabpanel)、状态(aria-selected、aria-expanded)和属性(aria-controls、aria-labelledby)告诉屏幕阅读器 widget 是什么和如何行为。正确用 ARIA 使自定义标签 widget 与原生不可区 分。错误用使 widget 比无 ARIA 更坏——不可键盘访问 div 上的 role="button" 告诉屏幕阅读器"这是按钮"但用户不能激活,比无标签 div 更挫。ARIA 第一规则是"无 ARIA 比坏 ARIA 好"——仅语义 HTML 不足时用且正确用。
何时使用
仅语义 HTML 不足时——自定义标签、手风琴、组合框、菜单、树视图、实时区域。若原生元素存在(button、select、details/summary)用它而非 ARIA。
如何做
1. 用 ARIA 前查原生元素是否存在 - 标签 → 简单情况原生 <details>/<summary>、复杂用 ARIA - 下拉 → 简单列表原生 <select>、搜索用 ARIA 组合框 - 对话 → 原生 <dialog> 元素(若浏览器支持足够) - 若原生工作用它——免费键盘可访问和屏幕阅读器宣读 2. 遵 ARIA 创作实践指南(APG)模式 - https://www.w3.org/WAI/ARIA/apg/ - 每模式指定: role、键盘交互、aria 属性 - 不自创 ARIA 模式——用文档的 3. 标签 widget(最常见自定义 widget): - 容器 role="tablist"、每标签 role="tab"、每面板 role="tabpanel" - 活动标签 aria-selected="true/false" - 标签 aria-controls="panel-id"、面板 aria-labelledby="tab-id" - 键盘: 方向左/右移标签、Tab 进面板 4. 手风琴: - 尽可能用原生 <details>/<summary>(无需 ARIA、默认工作) - 若自定义: 面板 role="region"、触发器 aria-expanded、aria-controls 5. 组合框(可搜索下拉): - 输入 role="combobox"、结果 role="listbox"、每项 role="option" - 输入 aria-expanded="true/false"、aria-controls="listbox-id" - 高亮项 aria-activedescendant="option-id" - 键盘: 方向下/上移、Enter 选、Escape 关 6. 实现后用屏幕阅读器测 - widget 正确宣其 role 和状态吗? - 键盘交互匹配 APG 模式吗? - widget 无任何视觉信息可用吗?
示例
// 标签 widget: 遵 ARIA APG 模式
function Tabs({ tabs }) {
const [activeIndex, setActiveIndex] = useState(0);
return (
<div>
<div role="tablist">
{tabs.map((tab, i) => (
<button
key={tab.id}
role="tab"
id={`tab-${tab.id}`}
aria-selected={i === activeIndex}
aria-controls={`panel-${tab.id}`}
tabIndex={i === activeIndex ? 0 : -1}
onClick={() => setActiveIndex(i)}
onKeyDown={handleTabKeyDown} // 方向左/右
>
{tab.label}
</button>
))}
</div>
{tabs.map((tab, i) => (
<div
key={tab.id}
role="tabpanel"
id={`panel-${tab.id}`}
aria-labelledby={`tab-${tab.id}`}
hidden={i !== activeIndex}
>
{tab.content}
</div>
))}
</div>
);
}
// 手风琴: 优先原生 <details>(无需 ARIA)
<details>
<summary>节标题</summary>
<div>节内容</div>
</details>
// 免费键盘、屏幕阅读器和展开/收起工作
// 组合框: 可搜索下拉
function Combobox({ options, value, onChange }) {
const [isOpen, setIsOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(-1);
return (
<div>
<input
role="combobox"
aria-expanded={isOpen}
aria-controls="combobox-listbox"
aria-activedescendant={activeIndex >= 0 ? `option-${activeIndex}` : undefined}
value={value}
onKeyDown={handleComboboxKeyDown} // 方向下/上/Enter/Escape
/>
{isOpen && (
<ul role="listbox" id="combobox-listbox">
{options.map((opt, i) => (
<li
key={opt.id}
role="option"
id={`option-${i}`}
aria-selected={i === activeIndex}
onClick={() => onChange(opt)}
>
{opt.label}
</li>
))}
</ul>
)}
</div>
);
}常见问题
- ⚠给 <div onClick> 加 role="button" 不使其键盘可访问——屏幕阅读器宣"按钮"但用户不能 Tab 到或激活,比无 ARIA 更坏。
- ⚠用 ARIA role 无对应键盘交互——role="tab" widget 不支持方向左/右宣为标签但不可作为标签操作,困惑屏幕阅读器用户。
- ⚠自创 ARIA role 或属性而非遵 APG 模式——自定义 ARIA 无屏幕阅读器解,widget 比无 ARIA 更不可访问。
- ⚠原生元素工作时用 ARIA"修"div——给 <div> 加 role="navigation" 和 aria-label 而 <nav> 元素免费提供同语义且默认键盘可访问。
最佳实践
- ✓原生 HTML 元素存在时尽可能用——免费提供 role、键盘支持和屏幕阅读器宣读,ARIA 仅自定义 widget 需。
- ✓每个自定义 widget 遵 ARIA 创作实践指南(APG)——模式用真实屏幕阅读器测并指定精确 role、状态、属性和键盘交互。
- ✓总将 ARIA role 配对应键盘交互——role="tab" 须支持方向左/右、role="combobox" 须支持 ArrowDown/Enter/Escape,否则 widget 宣为非其所是。
- ✓每个自定义 widget 至少用两个屏幕阅读器(NVDA + VoiceOver)测——ARIA 解不同,一个工作的 widget 另一个可能坏。