ARIA 状态与属性(aria-*)
ARIA 状态(states)和属性(properties)是 ARIA 规范的两大组成部分,它们通过 aria-* 全局属性为 HTML 元素提供丰富的语义信息。状态反映元素当前的条件(如 aria-checked="true"),属性则描述元素的特征或关系(如 aria-label="关闭")。本节将详细介绍最常用的 aria-* 属性及其使用场景。
前置知识
阅读本节前,建议先了解:ARIA 角色(role)
状态与属性的区别
| 类型 | 说明 | 示例 | 特点 |
|---|---|---|---|
| 状态(States) | 反映元素当前的条件,可由用户交互改变 | aria-checked、aria-expanded、aria-selected | 会随交互动态变化 |
| 属性(Properties) | 描述元素的特征,通常不会频繁变化 | aria-label、aria-labelledby、aria-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">×</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-onlyCSS 类
视觉隐藏但辅助技术可访问
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>注意事项
- 不要在原生元素上重复声明:
<button>已有隐含的按钮角色,不要添加role="button" - ARIA 属性实时同步:确保 DOM 操作后,ARIA 属性值与元素实际状态一致
- aria-hidden 不可滥用:确保被隐藏的元素不包含需要被辅助技术感知的信息
- 描述性文本优先:优先使用页面中的可见文本(
aria-labelledby),仅在必要时使用aria-label
最佳实践
- 为所有图标按钮提供
aria-label - 使用
aria-describedby关联表单字段的帮助文本和错误提示 - 使用
aria-expanded标记可展开/折叠的组件状态 - 为自定义交互组件同步更新所有相关 ARIA 状态
- 使用
aria-live区域通知用户动态内容变化(详见下一节) - 在开发阶段使用浏览器的 Accessibility Inspector 检查 ARIA 属性
下一节
继续学习:ARIA Live Regions