可访问性不能靠检查清单或CI拦截,因其依赖DOM结构、JavaScript行为与CSS状态三者同步;自定义组件须手动兑现4个契约;表单无label时ARIA补救需满足aria-labelledby指向可见稳定元素且设tabindex="-1";动态内容须用专用aria-live区域并正确配置aria-atomic和aria-relevant。

为什么“强制推行”不能靠检查清单或CI拦截
可访问性不是能用 aria-label 一加就完事的开关。很多团队把 WCAG 条款当 checklist 打钩,结果上线后键盘用户卡在 div 上按空格没反应、屏幕阅读器把图标按钮读成“空白按钮”、表单提交后焦点丢失——这些都不是 lint 工具能 catch 的问题。
真正卡点在于:可访问性行为依赖 DOM 结构 + JavaScript 行为 + CSS 状态三者同步。比如 aria-expanded="true" 和实际面板是否展开、tabindex 是否随禁用状态动态移除、role="button" 是否监听了 keydown 中的空格键——这些必须在组件生命周期里主动维护,而非一次性声明。
自定义组件中必须手动补全的 4 个可访问性契约
原生元素(如 button、input)自带语义和行为;自定义组件(如 x-toggle、x-tab)默认就是一堆无意义的 div,必须显式兑现以下契约:
- 继承
HTMLElement,且构造函数第一行调用super() - 声明
static formAssociated = true(仅对表单控件有效,如开关、输入框) - 在 Shadow DOM 内挂载真实
input[type="checkbox"]或button,并通过 getter/setter 桥接checked、disabled、value - 在
attributeChangedCallback中正确处理布尔属性:newValue !== null判断checked,而非!!newValue或newValue === "true"
表单控件不配 label 时的补救底线
如果因历史原因或设计限制无法用原生 label 包裹或关联 for/id,ARIA 补救必须满足两个条件,缺一不可:
立即学习“前端免费学习笔记(深入)”;
-
aria-labelledby指向一个**可见且稳定存在**的元素 ID(例如页面顶部的标题<h2 id="form-title">注册信息</h2>),不能指向 JS 动态插入后可能消失的节点 - 同时设置
tabindex="-1"在该被引用元素上,确保它不会意外抢走键盘焦点
用 aria-label 替代 label 是次选方案,仅限图标按钮等无文本控件;对表单输入框硬塞 aria-label,会导致屏幕阅读器完全忽略 placeholder 和错误提示,反而更难理解上下文。
动态内容更新时最容易被忽略的 aria-live 配置
通知类组件(如 toast、表单校验提示、搜索建议)若只靠视觉变化,键盘用户和屏幕阅读器根本感知不到。但随便加 aria-live="polite" 也容易出问题:
- 不要把
aria-live放在频繁变动的容器上(如整个main),否则每次微小 DOM 变动都会触发朗读,干扰主流程 - 推荐做法:单独创建一个
<div aria-live="polite" aria-atomic="false" aria-relevant="additions text"></div>作为专用播报区,所有动态消息都 append 到这里 -
aria-atomic="false"表示只朗读新增文本,避免重复播报整块区域;若需完整重读(如错误摘要),才设为true
最常漏掉的是 aria-relevant —— 默认值是 additions text,但如果你的提示会删掉旧文案再写新文案(比如从“密码太短”变成“密码已确认”),就必须显式加上 removals,否则旧消息残留不被清除,新消息也不播报。



















