role不是增强语义的工具,而是原生标签不适用时的兜底方案;仅三类情况需使用:自定义交互控件、无法重构的旧容器、动态渲染缺乏结构提示的区域。

为什么用 role 前得先确认原生标签不适用
绝大多数时候,role 不是“增强语义”的工具,而是“兜底方案”。浏览器对 <button></button>、<nav></nav>、<main></main> 等原生标签有内置行为和隐式 ARIA role(比如 <nav></nav> 自带 role="navigation"),强行加 role 可能覆盖默认逻辑,导致状态不同步。例如给 <input type="checkbox"> 加 role="switch",读屏器会误判为自定义开关,但原生 checkbox 的 aria-checked 状态仍由浏览器自动管理,JS 手动设值反而引发 AT 与 DOM 状态错位。
只有三类情况才真正需要 role:
- 用
div或span实现的交互控件(如自定义滑块、开关、tab) - 旧项目无法重构的容器结构(如遗留的
div class="menu") - 动态渲染区域缺乏结构性提示(如 JS 插入的实时日志区)
role="dialog" 必须配齐的三个属性
仅写 role="dialog" 是无效的。屏幕阅读器看到这个 role 就会进入模态上下文,但若缺关键属性,它要么跳过、要么报错、要么行为异常。
-
aria-modal="true":告诉辅助技术“当前焦点被锁在此区域”,否则用户仍可 tab 到背景内容 -
aria-labelledby:必须指向一个可见的标题元素 ID(如<h2 id="modal-title">确认删除</h2>),不能用aria-label替代——后者在部分读屏器中不触发标题朗读 -
tabindex="-1":确保模态框本身可被 JS 聚焦(el.focus()),否则键盘用户无法进入
漏掉任意一项,NVDA 或 VoiceOver 都可能把弹窗当普通层叠 div 处理,完全不提示“对话框已打开”。
立即学习“前端免费学习笔记(深入)”;
自定义下拉菜单的 role 和状态属性怎么配对
用 div 实现的下拉菜单不是加个 role="combobox" 就完事。读屏器看到这个 role,就期待配套的状态控制和交互反馈。
-
role="combobox"必须配aria-expanded="true/false",且该值要随点击/键盘操作实时同步 - 必须配
aria-controls="listbox-id",指向弹出列表容器的 ID(如<ul id="listbox-id" role="listbox">) - 弹出列表容器需设
role="listbox",每个选项用role="option",并设aria-selected="true/false" - 所有交互元素(触发按钮、选项)都要有
tabindex="0",并监听Enter、Space、↓/↑键
常见错误是只设 role="combobox",结果读屏器报“组合框”,但不说是否展开、不读选项、不支持方向键——等于给了个没说明书的遥控器。
aria-label 和 aria-labelledby 别混用,更别都写
两者功能重叠,但维护成本和兼容性差异很大。同时写会触发 Lighthouse 报错,且 aria-labelledby 优先级更高,aria-label 直接被忽略。
- 优先用
aria-labelledby:比如搜索框旁有<label id="search-label">查找商品</label>,输入框写aria-labelledby="search-label"—— 文案改了,读屏器自动同步 - 只在无对应可见文本时用
aria-label:如纯 SVG 图标按钮、加载 spinner - 绝对不要写
aria-label="提交" aria-labelledby="submit-label"—— 后者生效,前者白写,还污染代码
多语言站点尤其要注意:aria-label 写死英文,翻译系统根本不会处理;而 aria-labelledby 指向的 label 文本会被正常翻译。
真正难的不是记住哪些 role 要配哪些属性,而是每次加 role 之前,先问一句:这东西能不能用 <button></button>、<select></select>、<dialog></dialog> 原生标签重写?如果答案是能,那就别碰 role —— 它不是补丁,是最后一道防线。



















