Skip to content
返回质量保障

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 另一个可能坏。