语义化标签是无障碍访问的最低门槛,而非锦上添花;不用而用模拟按钮,或用替代,等于主动屏蔽屏幕阅读器用户。

语义化标签不是“锦上添花”,而是无障碍可访问性的最低门槛。不用 <button></button> 而用 <div> 模拟按钮,或用 <code><div class="nav"> 替代 <code><nav></nav>,等于主动屏蔽屏幕阅读器用户——这不是体验问题,是功能缺失。
为什么 <nav></nav> 不能被 <div role="navigation"> 替代
<p>两者在辅助技术中表现差异极大:<code><nav></nav> 是隐式带 role="navigation" 的原生元素,浏览器和屏幕阅读器(如 NVDA、VoiceOver)会自动识别并提供“跳转到导航区”快捷键;而手动加 role 的 <div> 缺少内置键盘焦点管理、无默认 tab-index、不响应空格/回车激活,且部分老旧 AT(如 IE11 + JAWS 组合)可能完全忽略该 role。
<ul>
<li>必须确保每个页面仅有一个主导航使用 <code><nav aria-label="主导航"></nav>,避免多个 <nav></nav> 无标识导致混淆
<nav aria-label="面包屑"></nav> 明确区分用途<nav></nav>:一个 <nav></nav> 内部不应再包含另一个 <nav></nav>
<main></main> 的唯一性与 DOM 位置陷阱
<main></main> 必须且只能出现一次,且应直接包裹页面主体内容——它不是“视觉居中容器”,也不是“样式 wrapper”。常见错误是把它放在某个 <div class="wrapper"> 内部,或与 <code><header></header>/<footer></footer> 并列但被其他非语义 <div> 包裹,导致屏幕阅读器无法准确定位主内容起始点。
<ul>
<li>推荐结构:<code> → <header></header> → <main></main> → <footer></footer>,中间不插入无语义容器
<section></section> 划分,而非多个 <main></main>
<div id="root"> 不应包裹 <code><main></main>,否则破坏其语义层级
表单中 <label></label> 关联失败的三个隐藏原因
即使写了 <label for="email">邮箱</label><input id="email">,仍可能被屏幕阅读器跳过——问题常不在语法,而在执行环境。
- ID 值含特殊字符(如
user-email@domain):HTML ID 不允许@,会导致for失效;应改为user_email - 动态生成的 input 缺少
id:React/Vue 中未显式设置id属性,或用Math.random()生成却未同步更新for值 - label 内容为空或仅含空格:某些 AT(尤其是移动端 VoiceOver)会跳过
<label> </label>,哪怕它已正确绑定
ARIA 属性不是语义化补丁,而是兜底手段
所有 ARIA(如 aria-label、aria-describedby)都应在原生语义不可用时才引入。滥用 ARIA 不仅增加维护成本,还可能覆盖浏览器默认行为,引发冲突。例如给 <button></button> 加 role="button" 是冗余甚至有害的——它会覆盖 <button></button> 自带的隐式 role 和键盘交互逻辑。
使用 Puppeteer + Chrome 将 HTML 渲染为中文 PDF,自动处理图表等待、Tab 展开、动画、测高、白边消除、防分页,适用于看板、报表、网页和交互图表转 PDF。
- 优先顺序永远是:原生标签 > 原生属性(如
required、disabled) > ARIA -
aria-live仅用于动态更新区域(如搜索建议、表单错误实时提示),不可用于静态内容 - 自定义组件(如手写的下拉菜单)必须同时实现 keyboard focus management(Tab/Shift+Tab)、arrow key 导航、Enter/Space 激活,否则仅加 ARIA role 仍是不可访问的
最易被忽略的点:语义化不是写完就结束的事。每次 DOM 变更(如模态框打开、列表过滤重绘),都要验证 <main></main> 是否仍为焦点主区域、<nav></nav> 是否仍可跳转、表单 label 是否仍能点击聚焦——这些在 JS 驱动的现代前端里,恰恰最容易悄然失效。










