eslint-plugin-jsx-a11y 与 html-validate 需要协同配合:前者专门检查 JSX 中的动态无障碍逻辑(例如 aria-hidden={isMenuOpen ? 'false' : 'true'}),后者则覆盖纯 HTML 片段(比如服务端渲染模板、CMS 输出)。两者互补,缺一不可,共同构建完整的前端无障碍质量检测门禁。

HTML 代码质量并非通过“重写一遍”就能自动提升,关键在于建立合理的约束机制与渐进式落地策略。如果缺乏统一的语义规范、未绑定构建流程、没有接入 CI 检查,那么重构后的代码 90% 会退回到原有的混乱状态。
为什么 eslint-plugin-jsx-a11y 与 html-validate 必须同时使用
仅靠 eslint-plugin-jsx-a11y 无法覆盖纯 HTML 片段——例如服务端渲染模板、CMS 输出块、以及遗留的 innerHTML 注入。反过来,单独使用 html-validate 也无法检测 JSX 层面的动态属性逻辑,一个典型的错误是 aria-hidden={isMenuOpen ? 'false' : 'true'} 写反了,这种错误在纯 HTML 扫描中根本无法暴露。
实操层面,可以这样搭配:
- 在 Webpack 或 Vite 构建流程中,使用
html-validate的 CLI 模式扫描src/**/*.html和public/**/*.html,并配置"valid-lang": "error"、"require-skip-link": "warn"等强约束规则 - 在 ESLint 配置中启用
jsx-a11y/aria-props、jsx-a11y/heading-has-content,并将rulesDirectory指向团队自定义规则——例如禁止div[role="button"]但未绑定onKeyDown的情况 - 在 CI 中同时将两者设为失败门禁:一个插件报
aria-required-child错误,另一个报duplicate-id,只要有一个不通过,就直接阻止 PR 合并
如何避免 data-* 属性成为新的混乱来源
重构时,常见做法是将旧 class 名批量替换为 data-component 或 data-track-id,但几个月后页面可能充满 data-xxx,其语义比 class 还要混乱。要避免这种情况,关键在于提前制定并限定规则。
实操建议:
- 限定前缀白名单:
data-ui-(仅用于组件边界识别)、data-track-(仅用于埋点)、data-test-(仅用于测试定位),其他前缀一律禁止 - 所有
data-ui-*属性必须在设计系统文档中注册,例如data-ui-tabs-container对应 Tabs 组件根节点,该值不可被业务层覆盖 - 使用 PostHTML 插件在构建时扫描非法
data-属性,匹配正则/^data-(?!ui|track|test)/并直接报错
在嵌套 和 时,什么情况下必须添加 aria-labelledby
当 缺少标题元素(),或者标题被视觉隐藏但未使用 –
aria-label 或 aria-labelledby 关联时,屏幕阅读器无法识别内容层级,导致整个区域被直接跳过。
常见错误场景:
- 仪表盘卡片使用
包裹,内部只有,但缺少 heading 标签用户统计
- 多步骤表单中每个
通过 CSS 隐藏了标题文字,仅靠图标示意
实操建议:
- 强制要求:任何没有
的–
或,必须包含aria-labelledby指向页面内真实存在的 ID,且该 ID 所在元素需含有可读文本(不能是空的) - 利用
html-validate的"require-valid-alt"规则,扩展为自定义规则"require-section-label",在 CI 中执行检查 - 在设计系统组件库中,
Section组件默认渲染,id 由组件自动生成并注入相应的,业务方不可绕过
真正阻碍 HTML 质量跃迁的,从来不是语法对错,而是「谁有权修改」和「改错了谁负责」——所有校验规则必须绑定到具体角色(设计师提交 PR 时触发无障碍扫描,前端提交时触发语义结构检查),否则再好的规则也只是一纸空文。
