直接落地无障碍须从首行html控制风险,90%问题源于模板结构:lang必须为zh-cn等具体值,title和h1需动态同步,每个nav须配具体aria-label。

直接落地无障碍,不是等设计稿定完再补,而是从第一行 HTML 开始控制风险。90% 的可访问性问题在模板和基础结构里就埋好了,后续靠 ARIA 补救效果差、维护成本高、还容易出错。
HTML 模板必须填满的三个语义锚点
模板不是“写完就能用”,它只提供骨架,关键语义信息必须手动注入,否则读屏器一读一个错:
-
lang属性不能空——中文站点必须写lang="zh-CN"(港澳台用zh-HK/zh-TW),否则 VoiceOver/NVDA 可能切错语音引擎,把“登录”读成“dēng lù”还是“dēng lù”都不可控 -
<title></title>和<h1></h1>不能写死——SPA 场景下,React 用useEffect(() => { document.title = ... }),Vue 用onMounted(() => { document.title = ... }),否则读屏器只读初始值,用户根本不知道当前在哪个页面 - 每个
<nav></nav>必须带具体aria-label——aria-label="主导航"可行,aria-label="Navigation"或留空等于没标;面包屑导航则必须用aria-label="面包屑导航",且当前页链接加aria-current="page"
表单组件怎么避免“看起来能用,实际读不出”
表单是投诉高发区,问题不在 JS 校验逻辑,而在最基础的 HTML 关联是否成立:
- 别靠视觉对齐假装关联:
<label>用户名</label><input id="user-name">漏掉for="user-name",或 ID 拼错(user_namevsusername),屏幕阅读器就完全无法把标签和输入框绑定 - 禁用状态只改 CSS 不够——必须加
disabled属性,否则键盘仍可聚焦、读屏器仍报“可编辑”;同理,必填项必须用required,而非仅靠星号或aria-required="true" - 错误提示不能只弹 Toast——验证失败时设
aria-invalid="true",并用aria-describedby="error-id"指向含错误文本的<div id="error-id">,确保读屏器播报完整上下文 <h3>自定义组件嵌入模板时的语义补位原则</h3> <p>模板里塞 <code><div class="tabs"> 这类“伪组件”,光加 ARIA 不够,得按真实交互逻辑补足整套语义链: <ul><li>用 <code><button></button>做 tab 按钮,别用<div role="button"> + <code>tabindex="0"+ 手动监听keydown——前者原生支持空格/Enter 激活、焦点环、禁用态,后者漏一环就卡死键盘用户 - 标签页内容区必须用
<section role="tabpanel"></section>,且每个role="tab"元素要配aria-controls="panel-id",指向对应面板的id;同时用aria-labelledby="tab-id"反向关联,让读屏器知道“这个面板是由哪个 tab 控制的” - 动态加载内容(如分页列表)必须加
aria-live="polite"区域,并设aria-atomic="true",否则 NVDA 可能只读新增部分,漏掉总数或状态变化
真正难的不是写对某一行 ARIA,而是所有模板片段(header.inc、footer.inc、form-template.html)都保持语义一致性——一处漏掉 lang 或 aria-label,所有引用它的页面都会在键盘导航或读屏播报时突然失联。











