应使用原生语义化标签替代div套壳,如nav、fieldset、section、main等,并精准使用aria角色补位,确保label正确关联、键盘焦点与动态状态同步,避免语义缺失或冗余。

用原生语义化标签替代 div 套壳
很多“自定义标签”其实是用 <div> + CSS 模拟的导航、表单组、内容区块,但屏幕阅读器完全不知道它是什么。真正可复用的“自定义”不是造新标签,而是选对已有语义化标签。<p>常见错误现象:<code><div class="nav"> 被 Lighthouse 报 “<code>Navigation element not marked up as navigation”;<div class="form-group"> 无法被识别为逻辑分组,导致屏幕阅读器逐个读 input,丢失上下文。<ul><li>
<code><nav></nav> 替代 <div class="nav">,哪怕只有一两个链接也应使用<li>
<code><fieldset><legend></legend></fieldset> 替代 <div class="checkbox-group">,尤其在多选/单选场景下不可省略<li>
<code><section></section> 必须带标题(<h2></h2> 或更高),否则语义断裂;若无可见标题,用 aria-labelledby 关联隐藏标题
<main></main> 页面中只能出现一次,且不应被 role="main" 重复声明——现代浏览器已原生支持,多余属性反而干扰当必须用 div/span 时,ARIA 角色要精准补位
不是所有组件都有原生标签(比如手风琴、标签页、树形菜单),这时 ARIA 是必要补充,但角色不能乱加。role 是“声明它是谁”,不是“让它变成谁”。
容易踩的坑:role="button" 加在 <div> 上却没处理 <code>Enter/Space 键响应;role="tablist" 容器里放了 <a></a> 却没设 tabindex="-1",导致焦点顺序错乱。
- 只在语义缺失时用 role:已有
<button></button>就别加role="button";已有<nav></nav>就别加role="navigation" -
role="tab"必须配合aria-controls(指向对应 panel 的 id)和aria-selected(动态切换 true/false) -
role="application"极其危险,会关闭屏幕阅读器大部分快捷键,仅限复杂富文本编辑器等极少数场景 - 避免
role="region"泛滥——它会把区域变成“地标”,过多会导致屏幕阅读器用户迷失
label 关联失效是高频可访问性破口
Lighthouse 报 “Form elements do not have associated labels” 不代表你没写 <label></label>,而是绑定链断了。这是自定义表单控件最常翻车的地方。
典型场景:用 <input type="checkbox"> 配合图标做开关按钮;用 <span></span> 模拟 radio 按钮;动态渲染后 ID 被 JS 修改但 for 属性没同步。
- ID 和
for值必须**完全一致**:大小写、连字符、下划线都不能差——id="user_email"对应for="user_email",不是for="userEmail" - 无可见文字的控件(如汉堡菜单按钮),优先用
aria-label,值写动词短语:aria-label="Open main menu",而非"Menu" - 避免
aria-label和<label></label>共存——前者会覆盖后者,造成语义冲突 - 用浏览器开发者工具的 Accessibility 面板直接查看元素的 “Name” 字段,确认是否解析出预期文本
键盘焦点与动态状态同步不能靠“差不多”
可访问性不是静态属性,而是交互过程中的实时状态表达。一个展开的手风琴面板,如果 aria-expanded="true" 没随点击更新,或者焦点没移到新展开的内容区,对键盘用户就是黑盒。
性能影响常被忽略:频繁操作 aria-* 属性会触发辅助技术重排,大量同步更新可能卡顿;而完全不更新,则导致状态失真。
- 所有可交互的自定义控件(哪怕只是
<div tabindex="0">)必须监听 <code>keydown,至少响应Enter和Space - 用
aria-live="polite"区域包裹动态变化的提示(如表单错误),但不要把它加在整页容器上——噪音太大 - 模态框打开时,用
inert属性或aria-hidden="true"隐藏背景内容,并将焦点 trap 在模态框内 - 避免用
display: none隐藏 tabpanel 内容——它会彻底移除可访问性树节点;改用hidden或aria-hidden="true"+ CSS 可见性控制
真实项目里,可访问性破口往往不在“没加 ARIA”,而在“加了但不同步”“写了但拼错 ID”“用了但没配键盘事件”。最麻烦的不是写代码,是让每个交互节点的状态、焦点、语义三者始终咬合。











