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

语义化标签不是“锦上添花”,而是无障碍可访问性的最低门槛。不用 <button> 而用 <div> 模拟按钮,或用 <div class="nav"> 替代 <nav>,等于主动屏蔽屏幕阅读器用户——这不是体验问题,是功能缺失。
为什么 <nav> 不能被 <div role="navigation"> 替代
两者在辅助技术中表现差异极大:<nav> 是隐式带 role="navigation" 的原生元素,浏览器和屏幕阅读器(如 NVDA、VoiceOver)会自动识别并提供“跳转到导航区”快捷键;而手动加 role 的 <div> 缺少内置键盘焦点管理、无默认 tab-index、不响应空格/回车激活,且部分老旧 AT(如 IE11 + JAWS 组合)可能完全忽略该 role。
- 必须确保每个页面仅有一个主导航使用
<nav aria-label="主导航">,避免多个<nav>无标识导致混淆 - 面包屑等次要导航应使用
<nav aria-label="面包屑">明确区分用途 - 不要嵌套
<nav>:一个<nav>内部不应再包含另一个<nav>
<main> 的唯一性与 DOM 位置陷阱
<main> 必须且只能出现一次,且应直接包裹页面主体内容——它不是“视觉居中容器”,也不是“样式 wrapper”。常见错误是把它放在某个 <div class="wrapper"> 内部,或与 <header>/<footer> 并列但被其他非语义 <div> 包裹,导致屏幕阅读器无法准确定位主内容起始点。
- 推荐结构:
<body>→<header>→<main>→<footer>,中间不插入无语义容器 - 若页面含多个逻辑区块(如仪表盘含“统计卡片”+“数据表格”),用
<section>划分,而非多个<main> - SSR 或动态渲染时注意:服务端注入的占位
<div id="root">不应包裹<main>,否则破坏其语义层级
表单中 <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> 加 role="button" 是冗余甚至有害的——它会覆盖 <button> 自带的隐式 role 和键盘交互逻辑。
- 优先顺序永远是:原生标签 > 原生属性(如
required、disabled) > ARIA -
aria-live仅用于动态更新区域(如搜索建议、表单错误实时提示),不可用于静态内容 - 自定义组件(如手写的下拉菜单)必须同时实现 keyboard focus management(Tab/Shift+Tab)、arrow key 导航、Enter/Space 激活,否则仅加 ARIA role 仍是不可访问的
最易被忽略的点:语义化不是写完就结束的事。每次 DOM 变更(如模态框打开、列表过滤重绘),都要验证 <main> 是否仍为焦点主区域、<nav> 是否仍可跳转、表单 label 是否仍能点击聚焦——这些在 JS 驱动的现代前端里,恰恰最容易悄然失效。



















