语义化标签不可替代div,必须用<header><nav><main>等定义结构;跳过链接须为首个可聚焦元素并指向<main>;ARIA是补丁而非替代,禁用冗余role;动态内容需手动管理焦点。

语义化标签必须替代 div 做结构划分
HTML5 的 <header>、<nav>、<main>、<article>、<section>、<aside>、<footer> 不是“可选装饰”,而是屏幕阅读器识别页面逻辑结构的唯一依据。用 <div> 套一堆 class 模拟导航或主内容,等于主动屏蔽辅助技术。
实操建议:
- 每个页面有且仅有一个
<main>,且不能嵌套在<article>或<section>内 -
<nav>应包含有意义的导航链接,纯装饰性图标栏别硬套<nav> -
<section>必须带aria-labelledby或<h2>–<h6>作为标题,否则会被读作“无名区域” - 避免
<header>套整个页眉+logo+搜索框+用户菜单——它只应包裹与当前<article>或页面相关的头部内容;全局页眉建议用<header role="banner">
跳过链接(skip link)不是可选项
键盘用户无法用鼠标点“返回顶部”,也无法靠视觉扫出“主内容从哪开始”。没有 <a href="#main">跳至主内容</a>,等于强制所有人从 logo、主导航、横幅广告逐个 tab 过去。
关键细节:
立即学习“前端免费学习笔记(深入)”;
- 跳过链接必须是页面第一个可聚焦元素(DOM 顺序第一),且默认隐藏(
position: absolute; left: -999px;),获得焦点时才显示 - 目标锚点必须是
<main id="main">或其他语义化主容器,不能是<div id="main"> - 若页面含多个
<main>(如 SPA 多视图),需动态更新跳过链接的href值
ARIA 属性不能乱加,尤其 role 和 aria-* 的组合
很多开发者看到“无障碍”就堆 role="region"、aria-label、aria-hidden="true",结果反而干扰阅读顺序或重复播报。ARIA 是补丁,不是语义替代品。
高频错误:
- 给已有语义的标签再加
role:如<nav role="navigation">——<nav>本身已映射为 navigation role,冗余声明可能被某些旧读屏忽略或覆盖 -
aria-hidden="true"用在包含焦点元素的容器上(如弹窗遮罩层里塞了<button>),会导致键盘焦点进入“黑洞” -
aria-live区域未设polite/assertive,或频繁更新导致播报打断用户操作 - 用
aria-label覆盖可见文本时,没同步更新 DOM 文本(例如按钮文字变了但 aria-label 没改),造成信息不一致
焦点管理在动态内容中极易失效
单页应用、模态框、手风琴菜单、Tab 切换组件——这些地方一旦 JS 插入/移除 DOM,浏览器不会自动重置焦点位置。用户按 Tab 键可能直接跳到地址栏,或卡在不可见元素上。
必须手动干预的场景:
- 模态框打开后,立即将焦点捕获到第一个可聚焦子元素(如
<input>或确认按钮),并用inert或aria-hidden="true"+tabindex="-1"锁定背景内容 - 关闭模态框后,焦点必须回到触发它的按钮(需提前缓存
document.activeElement) - Tab 切换时,新激活面板需包含至少一个可聚焦元素,且该元素应获得焦点(而非只靠 CSS
:focus-within) - 使用
scrollIntoView({ block: 'nearest', focus: true })替代简单focus(),避免焦点落在视口外
最常被忽略的是:当用 display: none 隐藏内容时,它仍保留在焦点流中;应改用 hidden 属性或 aria-hidden="true" + tabindex="-1" 组合清理。










