前端无障碍访问 a11y:语义化、ARIA 与焦点管理

FreeGuideOnline 最新 2026-06-15

搭建你的第一个无障碍网页:HTML语义化实战

什么是语义化?为什么它对无障碍至关重要?

语义化是指使用正确的HTML标签来描述内容的含义,而不仅仅是外观。一个 <div> 可以承载任何内容,但它对屏幕阅读器来说毫无意义。而像 <nav><main><button> 这样的标签自带“角色”,能自动传递信息给辅助技术。

核心好处:

  • 自动获得键盘可操作性<button> 天然支持 Enter 和 Space 键触发,而 <div onclick> 却无法响应键盘。
  • 自动暴露名称与状态<a> 标签会暴露链接文本作为名称,<input type="checkbox"> 会暴露选中状态。
  • 自动构建页面大纲<h1><h6> 以及区域标签(<section><article>)帮助用户快速导航。

对比示例:

<!-- 很差:无意义的 div -->
<div class="btn" onclick="submit()">提交</div>

<!-- 优秀:原生语义 -->
<button type="submit">提交</button>

语义化结构骨架:用区域标签构建页面

一个无障碍页面应当像一本书一样拥有清晰的目录。使用以下标签划分区块,并确保每个区块有唯一的可识别名称。

标签 含义 常用命名方式
<header> 页眉区域 通常包含网站Logo、导航
<nav> 主导航区块 使用 aria-label="主导航" 区分多个导航
<main> 页面主要内容 每个页面必须有且只有一个 <main>
<section> 独立的内容区块 建议搭配标题 <h2>
<article> 可独立分发的内容 如博文、评论卡片
<aside> 侧边栏/补充内容
<footer> 页脚区域

典型页面结构示例:

<body>
  <header>
    <h1>我的博客</h1>
    <nav aria-label="主导航">
      <ul>
        <li><a href="/">首页</a></li>
        <li><a href="/about">关于</a></li>
      </ul>
    </nav>
  </header>
  
  <main>
    <article>
      <h2>文章标题</h2>
      <p>内容...</p>
    </article>
  </main>

  <footer>
    <p>© 2025 我的博客</p>
  </footer>
</body>

让表单无障碍:标签与输入框的正确关联

表单是交互密集区域,错误频发。请务必让每个输入控件都有明确的标签。

  1. 显式关联 <label>:通过 for 属性匹配输入控件的 id
  2. 包裹式关联:将输入控件直接放在 <label> 内部。
  3. 占位符限制placeholder 不能替代 <label>,因为它会在输入时消失,且颜色对比度通常不足。
<!-- 推荐:显式关联 -->
<label for="email">邮箱地址</label>
<input type="email" id="email" name="email">

<!-- 同样推荐:包裹关联 -->
<label>
  用户名
  <input type="text" name="username">
</label>

<!-- 错误示范:仅用 placeholder -->
<input type="text" placeholder="搜索...">

图片与图标:alt 属性的艺术

每张 <img> 都必须有 alt 属性,但它的值应根据上下文确定:

  • 信息性图片:描述图片含义,如 <img src="pie.png" alt="销售占比:电子产品35%,服装25%">
  • 装饰性图片:使用空 alt,alt=""。屏幕阅读器将完全忽略它,减少噪音。
  • SVG 图标:需要额外照顾,通常给 <svg> 添加 aria-hidden="true" 并对容器提供文本替代。
<!-- 装饰图:空 alt -->
<img src="decorative-line.png" alt="">

<!-- 功能性图标按钮 -->
<button aria-label="关闭">
  <svg aria-hidden="true" focusable="false">...</svg>
</button>

ARIA:为动态界面补充语义

什么时候应该使用 ARIA?

ARIA(Accessible Rich Internet Applications)是一套属性,用于增强HTML的表意能力。一条黄金法则:尽量使用原生HTML语义,只有在原生HTML无法满足时才使用ARIA。

需要 ARIA 的典型场景:

  • 自定义交互组件(如选项卡、菜单、对话框、进度条)
  • 动态更新的内容区域(实时通知用户)
  • 修复不完美的语义标记(但更好的办法是改用正确的标签)

用 ARIA 属性描述自定义组件

角色(role)

role 属性覆盖元素的默认角色。例如,将一个无序列表转变为选项卡列表:

<div role="tablist" aria-label="作品分类">
  <button role="tab" aria-selected="true" aria-controls="panel1">全部</button>
  <button role="tab" aria-selected="false" aria-controls="panel2">设计</button>
  <button role="tab" aria-selected="false" aria-controls="panel3">开发</button>
</div>
<div id="panel1" role="tabpanel">...</div>
<div id="panel2" role="tabpanel" hidden>...</div>
<div id="panel3" role="tabpanel" hidden>...</div>

状态与属性

  • aria-expanded:表示可折叠元素是否展开,用于手风琴和菜单按钮。
  • aria-hidden="true":将元素对辅助技术完全隐藏,常用于装饰性或暂时隐藏内容。
  • aria-live:用于动态更新区域,如聊天、通知、搜索建议。取值 polite(读完当前内容再播报)或 assertive(立即播报)。
  • aria-label / aria-labelledby:为元素提供可访问名称,前者直接命名,后者引用其他元素的文本ID。

ARIA 实时区域示例:

<div id="status-message" aria-live="polite" aria-atomic="true">
  <!-- 动态插入的消息会立即被播报 -->
</div>

建立描述关系:aria-describedby

当需要有额外描述信息(如输入要求、错误提示)时,使用 aria-describedby 将输入控件与描述文字关联。

<label for="password">密码</label>
<input 
  type="password" 
  id="password" 
  aria-describedby="pwd-rule"
>
<span id="pwd-rule">至少包含8个字符,大小写字母加数字</span>

焦点管理:键盘用户的导航生命线

可聚焦元素与 tabindex 的正确用法

默认可通过Tab键聚焦的元素有:链接、按钮、表单控件以及设置了 tabindex="0" 的元素。tabindex 的值决定焦点顺序:

  • tabindex="0":将元素插入到自然的Tab顺序中。适合让 <div> 变得可聚焦。
  • tabindex="-1":元素可通过JavaScript的 element.focus() 聚焦,但不参与键盘Tab导航。通常用于模态对话框内的元素或需要编程移动焦点时。
  • 永远不要使用大于0的 tabindex,它会强行改变焦点顺序,破坏用户预期的导航路径,尤其在响应式布局中极易混乱。

焦点指示器:绝对不可移除

所有可交互元素在获得焦点时都必须有可见的焦点轮廓。许多人会写 :focus { outline: none; },这是最大的无障碍错误。

如果你因设计需要调整轮廓,请创建一个自定义的、清晰可见的焦点样式,而不是删除它。

:focus-visible {
  outline: 3px solid #2b7ce6;
  outline-offset: 2px;
}

优雅处理模态对话框的焦点

打开对话框后,必须将焦点移动至对话框内部,并限制焦点在对话框内循环,关闭时再将焦点返回到触发按钮。

核心步骤:

  1. 打开对话框:保存当前焦点元素引用,将焦点移至对话框内第一个可聚焦元素。
  2. 焦点陷阱:在对话框的Tab键事件中,确保焦点只在对话框内循环。
  3. 关闭对话框:将焦点恢复到打开前的元素。
// 焦点陷阱核心逻辑伪代码
dialog.addEventListener('keydown', (e) => {
  if (e.key === 'Tab') {
    const focusableElements = dialog.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    const first = focusableElements[0];
    const last = focusableElements[focusableElements.length - 1];

    if (e.shiftKey) { // 反向Tab
      if (document.activeElement === first) {
        e.preventDefault();
        last.focus();
      }
    } else { // 正向Tab
      if (document.activeElement === last) {
        e.preventDefault();
        first.focus();
      }
    }
  }
});

跳过导航链接:提升操作效率

为使用键盘的用户提供“跳到主要内容”的链接,避免每次都要遍历整个导航栏。这个链接通常是页面第一个可聚焦元素,并在获得焦点时显示。

<body>
  <a href="#main-content" class="skip-link">跳到主要内容</a>
  
  <header>...</header>
  
  <main id="main-content">
    ...
  </main>
</body>

<style>
.skip-link {
  position: absolute;
  top: -40px;
  left: 6px;
  background: #000;
  color: #fff;
  padding: 8px;
  z-index: 100;
}
.skip-link:focus {
  top: 6px;
}
</style>

从开发到测试:让无障碍融入你的工作流

键盘测试清单

完成一个组件后,立即用键盘完整走一遍。

  • 所有交互元素都能通过 Tab 抵达,并通过 Shift+Tab 返回。
  • 按钮能用 Enter 或 Space 激活。
  • 下拉菜单能用 Enter 展开,用 Escape 关闭,用箭头键导航。
  • 模态对话框打开时焦点移入、关闭时焦点归还。
  • 没有出现“键盘陷阱”(无法通过键盘离开的区域)。

屏幕阅读器快速验证

使用 VoiceOver (macOS) 或 NVDA (Windows) 体验:

  1. 开启屏幕阅读器,用 Ctrl+Option+U (Mac) 或 Insert+F7 打开元素列表,检查标题和链接层级。
  2. 浏览整个页面,听元素名称、角色和状态的播报。例如“邮件输入框”而不是“编辑文本”。
  3. 操作动态区域,确认消息是否实时播报。

自动化工具辅助检查

  • axe DevTools:浏览器扩展,自动检测WCAG违规,给出修复建议。
  • HTML 验证:确保文档有效,避免因为标签未闭合或属性错误导致解析异常。
  • 对比度检查器:确保文本与背景的对比度至少为4.5:1。

总结

前端无障碍不是附加功能,而是构建优质用户体验的基石。从选择正确的HTML标签开始,善用ARIA弥补组件语义的不足,并精心设计焦点流转,你已经能够创造出包容、可信赖的界面。将这些实践融入日常编码习惯,你写下的每一行代码都在让网络世界变得更加开放。

记住:无障碍是过程,而非终点。用心感受用户的多样化需求,你的应用将会被更多人喜爱。