Skip to content

ARIA 状态与属性(aria-*)

ARIA 状态(states)和属性(properties)是 ARIA 规范的两大组成部分,它们通过 aria-* 全局属性为 HTML 元素提供丰富的语义信息。状态反映元素当前的条件(如 aria-checked="true"),属性则描述元素的特征或关系(如 aria-label="关闭")。本节将详细介绍最常用的 aria-* 属性及其使用场景。

前置知识

阅读本节前,建议先了解:ARIA 角色(role)

状态与属性的区别

类型说明示例特点
状态(States)反映元素当前的条件,可由用户交互改变aria-checkedaria-expandedaria-selected会随交互动态变化
属性(Properties)描述元素的特征,通常不会频繁变化aria-labelaria-labelledbyaria-describedby相对稳定,通常在初始化时设置

注意

在 ARIA 1.2 中,"状态"和"属性"在技术实现上已经合并为"ARIA 属性",但概念上仍然有区分价值。两者都使用 aria-* 格式的 HTML 属性。

命名相关属性

aria-label

为元素提供可访问的名称,通常用于没有可见文本标签的元素:

html
<!-- 搜索按钮只有图标,需要 aria-label -->
<button type="submit" aria-label="搜索">
  <svg aria-hidden="true" width="20" height="20"><!-- 搜索图标 --></svg>
</button>

<!-- 关闭按钮 -->
<button aria-label="关闭对话框" onclick="closeDialog()">
  <span aria-hidden="true">&times;</span>
</button>

<!-- 自定义导航 -->
<nav aria-label="面包屑导航">
  <ol>
    <li><a href="/">首页</a></li>
    <li><a href="/products">产品</a></li>
  </ol>
</nav>

aria-labelledby

使用页面中其他可见元素的文本作为当前元素的标签:

html
<!-- 使用 id 关联可见标题 -->
<div>
  <h2 id="dialog-title">确认删除</h2>
  <p>此操作不可撤销,确定要删除这个文件吗?</p>
</div>
<div role="dialog" aria-labelledby="dialog-title" aria-describedby="dialog-desc">
  <!-- 关联标题和描述 -->
</div>
html
<!-- 表单字段的 label 关联 -->
<div>
  <span id="name-label">姓名</span>
  <input type="text" aria-labelledby="name-label" aria-required="true" />
</div>

<!-- 组合多个元素作为标签 -->
<h3 id="rating-title">用户评分</h3>
<p id="rating-desc">1 到 5 星</p>
<div role="slider"
     aria-labelledby="rating-title"
     aria-describedby="rating-desc"
     aria-valuemin="1"
     aria-valuemax="5"
     aria-valuenow="3">
</div>

aria-describedby

使用其他元素的文本作为当前元素的补充描述:

html
<!-- 表单字段关联错误提示 -->
<label for="password">密码</label>
<input type="password" id="password" aria-describedby="password-hint password-error" />
<span id="password-hint">至少 8 个字符,包含大小写字母和数字</span>
<span id="password-error" role="alert" style="color:red">密码强度不足</span>

aria-label vs aria-labelledby vs aria-describedby

属性用途文本来源优先级
aria-labelledby主标签页面中其他可见元素的文本最高
aria-label主标签(无可见文本时)属性值中直接提供
aria-describedby补充描述页面中其他可见元素的文本仅补充,不替代标签
html
<!-- 三者关系示例 -->
<div id="card-title">订单确认</div>
<div id="card-desc">请核对以下订单信息</div>

<div aria-labelledby="card-title" aria-describedby="card-desc" aria-label="订单确认对话框">
  <p>商品:无线耳机 x 1</p>
  <p>金额:¥299.00</p>
</div>

<!-- 屏幕阅读器播报顺序:
     1. 首先播报 aria-labelledby 指向的 "订单确认"
     2. 然后播报 aria-label 的 "订单确认对话框"(如果已设置)
     3. 最后播报 aria-describedby 指向的补充描述
-->

状态相关属性

aria-expanded

表示元素当前是否展开,常用于折叠面板、下拉菜单等:

html
<!-- 手风琴组件 -->
<button aria-expanded="false" aria-controls="section-1">
  常见问题
</button>
<div id="section-1" hidden>
  <p>这里是常见问题的答案...</p>
</div>

<script>
  document.querySelector('button').addEventListener('click', function() {
    const isExpanded = this.getAttribute('aria-expanded') === 'true';
    this.setAttribute('aria-expanded', String(!isExpanded));

    const content = document.getElementById('section-1');
    content.hidden = isExpanded;
  });
</script>

aria-checked

表示复选框或单选按钮的选中状态:

html
<!-- 自定义复选框 -->
<div role="checkbox" aria-checked="false" tabindex="0"
     onclick="toggleCheckbox(this)"
     onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleCheckbox(this)}">
  我已阅读并同意服务条款
</div>

<script>
function toggleCheckbox(el) {
  const isChecked = el.getAttribute('aria-checked') === 'true';
  el.setAttribute('aria-checked', String(!isChecked));
}
</script>

<!-- 自定义单选组 -->
<div role="radiogroup" aria-label="配送方式">
  <div role="radio" aria-checked="true" tabindex="0">标准配送(3-5天)</div>
  <div role="radio" aria-checked="false" tabindex="-1">快速配送(1-2天)</div>
  <div role="radio" aria-checked="false" tabindex="-1">当日达</div>
</div>

aria-selected

表示选项卡或列表项的选中状态:

html
<div role="listbox" aria-label="选择城市">
  <div role="option" aria-selected="false">北京</div>
  <div role="option" aria-selected="true">上海</div>
  <div role="option" aria-selected="false">广州</div>
</div>

aria-disabled

表示元素当前不可交互:

html
<!-- 禁用状态按钮 -->
<button aria-disabled="true" onclick="submitForm()">
  提交中...
</button>

<!-- 注意:aria-disabled 与 disabled 属性不同 -->
<!-- disabled 属性会从 Tab 序列中移除元素 -->
<!-- aria-disabled 保留元素在 Tab 序列中,但标记为不可操作 -->

aria-hidden

隐藏元素或元素内容,使其对辅助技术不可见:

html
<!-- 隐藏装饰性图标 -->
<button>
  <svg aria-hidden="true" width="16" height="16"><!-- 心形图标 --></svg>
  收藏
</button>

<!-- 隐藏重复文本 -->
<span class="sr-only">首页</span>
<a href="/" aria-hidden="true">Home</a>

<!-- 隐藏整个区域 -->
<div aria-hidden="true">
  <!-- 这个区域的内容不会被屏幕阅读器播报 -->
  <p>这段文字对辅助技术不可见</p>
</div>

<!-- 使用 hidden 属性时,元素对所有用户都不可见 -->
<div hidden>
  <!-- 完全隐藏,不在渲染树中 -->
</div>

aria-hidden 的注意事项

  • 不要在获得焦点的元素上使用 aria-hidden="true"
  • 不要在包含可聚焦元素的容器上使用 aria-hidden="true"
  • aria-hidden 不影响视觉显示,仅影响辅助技术
  • 如需视觉隐藏但辅助技术可访问,使用 .sr-only CSS 类

视觉隐藏但辅助技术可访问

html
<style>
  /* 屏幕阅读器专用样式 */
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
</style>

<!-- 视觉隐藏但屏幕阅读器可访问 -->
<a href="#main-content" class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:p-2 focus:bg-white focus:outline">
  跳到主要内容
</a>

<!-- 提供额外上下文 -->
<button>
  <svg aria-hidden="true"><!-- 删除图标 --></svg>
  <span class="sr-only">删除文件</span>
</button>

值相关属性

aria-valuenow / aria-valuemin / aria-valuemax

用于表示范围内控件的当前值:

html
<!-- 进度条 -->
<div role="progressbar"
     aria-valuenow="60"
     aria-valuemin="0"
     aria-valuemax="100"
     aria-label="上传进度 60%">
  <div style="width: 60%; background: #4caf50; height: 20px;"></div>
</div>

<!-- 滑块 -->
<div role="slider"
     tabindex="0"
     aria-label="音量"
     aria-valuenow="50"
     aria-valuemin="0"
     aria-valuemax="100"
     aria-valuetext="50%">
</div>

aria-valuetext

为值提供人类可读的文本描述:

html
<!-- 温度滑块 -->
<input type="range" min="16" max="30" value="22"
       aria-label="室内温度"
       aria-valuetext="22 摄氏度,舒适温度" />

<!-- 日历选择器 -->
<div role="slider"
     aria-valuenow="3"
     aria-valuetext="三月"
     aria-valuemin="1"
     aria-valuemax="12">
</div>

关系属性

aria-controls

声明当前元素控制的目标元素:

html
<!-- 手风琴:按钮控制面板的展开/折叠 -->
<button aria-expanded="true" aria-controls="panel-content">
  查看详情
</button>
<div id="panel-content">
  <p>这是展开的详细内容...</p>
</div>

aria-owns

声明元素的逻辑子元素(当子元素不在 DOM 层级中直接嵌套时):

html
<!-- 当子元素在 DOM 中不直接嵌套时 -->
<div role="combobox" aria-owns="listbox-1" aria-expanded="true">
  <input type="text" />
</div>
<ul id="listbox-1" role="listbox">
  <li role="option">选项 A</li>
  <li role="option">选项 B</li>
</ul>

aria-flowto

定义阅读顺序,用于非线性布局:

html
<!-- 复杂布局中的阅读顺序 -->
<div id="sidebar" aria-flowto="main-content">
  侧边栏内容...
</div>

<div id="main-content">
  主要内容...
</div>

aria-haspopup

指示元素是否有弹出内容:

html
<!-- 下拉菜单按钮 -->
<button aria-haspopup="true" aria-expanded="false">
  更多选项
</button>

<!-- 更精确的类型声明 -->
<button aria-haspopup="menu" aria-expanded="false">
  操作菜单
</button>

<button aria-haspopup="dialog" aria-expanded="false">
  打开设置
</button>

<button aria-haspopup="listbox" aria-expanded="false">
  选择颜色
</button>

aria-posinset / aria-setsize

表示在集合中的位置和总数量:

html
<!-- 搜索结果列表 -->
<div role="list" aria-label="搜索结果">
  <div role="listitem" aria-posinset="1" aria-setsize="10">
    结果 1
  </div>
  <div role="listitem" aria-posinset="2" aria-setsize="10">
    结果 2
  </div>
  <!-- 更多结果... -->
</div>

其他常用属性

aria-required

标记表单字段为必填:

html
<label for="email">电子邮箱 *</label>
<input type="email" id="email" aria-required="true" />

aria-invalid

标记表单字段的值是否无效:

html
<label for="email">电子邮箱</label>
<input type="email" id="email"
       aria-required="true"
       aria-invalid="true"
       aria-describedby="email-error" />
<span id="email-error" role="alert">请输入有效的邮箱地址</span>

aria-errormessage

直接关联错误消息元素:

html
<label for="username">用户名</label>
<input type="text" id="username"
       aria-errormessage="username-error"
       aria-invalid="true" />
<span id="username-error">用户名已被占用</span>

aria-busy

表示区域内容正在更新中:

html
<div aria-busy="true" aria-live="polite">
  <!-- 屏幕阅读器等待 aria-busy 变为 false 后再播报变化 -->
  <p>正在加载最新数据...</p>
</div>

aria-pressed

表示切换按钮的按下状态:

html
<!-- 加粗按钮 -->
<button aria-pressed="false" onclick="toggleBold(this)">
  B
</button>
<button aria-pressed="true" onclick="toggleItalic(this)">
  I
</button>

实战示例:无障碍的自定义下拉菜单

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>无障碍自定义下拉菜单</title>
  <style>
    .dropdown { position: relative; display: inline-block; }

    .dropdown-trigger {
      padding: 0.5rem 1rem;
      border: 1px solid #ccc;
      background: #fff;
      cursor: pointer;
    }

    .dropdown-menu {
      position: absolute;
      top: 100%;
      left: 0;
      min-width: 200px;
      border: 1px solid #ddd;
      background: #fff;
      list-style: none;
      padding: 0;
      margin: 0;
      display: none;
      z-index: 100;
    }

    .dropdown-menu[aria-hidden="false"] {
      display: block;
    }

    .dropdown-menu li a {
      display: block;
      padding: 0.5rem 1rem;
      text-decoration: none;
      color: #333;
    }

    .dropdown-menu li a:hover,
    .dropdown-menu li a[aria-current="true"] {
      background: #e0f0ff;
    }
  </style>
</head>
<body>
  <div class="dropdown">
    <!-- 触发按钮 -->
    <button class="dropdown-trigger"
            aria-haspopup="listbox"
            aria-expanded="false"
            aria-labelledby="selected-value trigger-label">
      <span id="trigger-label">排序方式:</span>
      <span id="selected-value">最新发布</span>
    </button>

    <!-- 下拉选项列表 -->
    <ul class="dropdown-menu"
        role="listbox"
        aria-labelledby="trigger-label"
        aria-activedescendant=""
        aria-hidden="true">
      <li role="option" id="option-latest" aria-selected="true">
        <a href="#" data-value="latest">最新发布</a>
      </li>
      <li role="option" id="option-popular" aria-selected="false">
        <a href="#" data-value="popular">最受欢迎</a>
      </li>
      <li role="option" id="option-price-asc" aria-selected="false">
        <a href="#" data-value="price-asc">价格从低到高</a>
      </li>
      <li role="option" id="option-price-desc" aria-selected="false">
        <a href="#" data-value="price-desc">价格从高到低</a>
      </li>
    </ul>
  </div>

  <script>
    const trigger = document.querySelector('.dropdown-trigger');
    const menu = document.querySelector('.dropdown-menu');
    const selectedValue = document.getElementById('selected-value');
    const options = menu.querySelectorAll('[role="option"]');

    // 打开/关闭菜单
    trigger.addEventListener('click', toggleMenu);

    function toggleMenu() {
      const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
      trigger.setAttribute('aria-expanded', String(!isExpanded));
      menu.setAttribute('aria-hidden', String(isExpanded));

      if (!isExpanded) {
        // 打开时,聚焦到选中的选项
        const selected = menu.querySelector('[aria-selected="true"]');
        if (selected) {
          menu.setAttribute('aria-activedescendant', selected.id);
        }
      }
    }

    // 选择选项
    options.forEach(option => {
      option.querySelector('a').addEventListener('click', function(e) {
        e.preventDefault();
        selectOption(option);
      });
    });

    function selectOption(option) {
      // 更新选中状态
      options.forEach(o => o.setAttribute('aria-selected', 'false'));
      option.setAttribute('aria-selected', 'true');
      selectedValue.textContent = option.querySelector('a').textContent;

      // 关闭菜单
      trigger.setAttribute('aria-expanded', 'false');
      menu.setAttribute('aria-hidden', 'true');
    }

    // 键盘导航
    trigger.addEventListener('keydown', handleTriggerKeydown);
    menu.addEventListener('keydown', handleMenuKeydown);

    function handleTriggerKeydown(e) {
      if (e.key === 'ArrowDown' || e.key === 'Space') {
        e.preventDefault();
        const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
        if (!isExpanded) toggleMenu();
      }
    }

    function handleMenuKeydown(e) {
      const currentId = menu.getAttribute('aria-activedescendant');
      const currentIndex = Array.from(options).findIndex(o => o.id === currentId);

      switch (e.key) {
        case 'ArrowDown':
          e.preventDefault();
          focusOption((currentIndex + 1) % options.length);
          break;
        case 'ArrowUp':
          e.preventDefault();
          focusOption((currentIndex - 1 + options.length) % options.length);
          break;
        case 'Enter':
        case ' ':
          e.preventDefault();
          if (currentId) selectOption(document.getElementById(currentId));
          break;
        case 'Escape':
          e.preventDefault();
          trigger.setAttribute('aria-expanded', 'false');
          menu.setAttribute('aria-hidden', 'true');
          trigger.focus();
          break;
      }
    }

    function focusOption(index) {
      const option = options[index];
      menu.setAttribute('aria-activedescendant', option.id);
    }

    // 点击外部关闭菜单
    document.addEventListener('click', function(e) {
      if (!e.target.closest('.dropdown')) {
        trigger.setAttribute('aria-expanded', 'false');
        menu.setAttribute('aria-hidden', 'true');
      }
    });
  </script>
</body>
</html>

注意事项

  1. 不要在原生元素上重复声明<button> 已有隐含的按钮角色,不要添加 role="button"
  2. ARIA 属性实时同步:确保 DOM 操作后,ARIA 属性值与元素实际状态一致
  3. aria-hidden 不可滥用:确保被隐藏的元素不包含需要被辅助技术感知的信息
  4. 描述性文本优先:优先使用页面中的可见文本(aria-labelledby),仅在必要时使用 aria-label

最佳实践

  • 为所有图标按钮提供 aria-label
  • 使用 aria-describedby 关联表单字段的帮助文本和错误提示
  • 使用 aria-expanded 标记可展开/折叠的组件状态
  • 为自定义交互组件同步更新所有相关 ARIA 状态
  • 使用 aria-live 区域通知用户动态内容变化(详见下一节)
  • 在开发阶段使用浏览器的 Accessibility Inspector 检查 ARIA 属性

下一节

继续学习:ARIA Live Regions

参考链接