屏幕阅读器支持
语义化 HTML 标签是 Web 可访问性的基石。屏幕阅读器(Screen Reader)是视障用户访问网页的主要工具,语义化标签为屏幕阅读器提供了丰富的结构信息,让用户能够高效地导航和理解页面内容。
前置知识
阅读本节前,建议先了解:SEO 优化建议
基础概念
屏幕阅读器如何工作
屏幕阅读器是一种辅助技术(Assistive Technology),它将屏幕上的视觉信息转换为语音或盲文输出。屏幕阅读器通过以下方式理解页面:
- DOM 树解析:读取 HTML 元素的结构
- 可访问性树(Accessibility Tree):根据 HTML 语义构建可访问性树
- ARIA 属性:读取额外的无障碍信息
- 语义化标签:根据标签类型提供特定播报
常见屏幕阅读器
| 屏幕阅读器 | 平台 | 特点 |
|---|---|---|
| NVDA | Windows | 免费开源,功能强大 |
| JAWS | Windows | 商业软件,企业广泛使用 |
| VoiceOver | macOS/iOS | Apple 系统内置 |
| TalkBack | Android | Android 系统内置 |
| Narrator | Windows | Windows 系统内置 |
语法与使用
Landmark 导航
屏幕阅读器用户可以通过快捷键在不同 landmark 之间跳转:
| Landmark | 标签 | NVDA 快捷键 | 说明 |
|---|---|---|---|
| Banner | <header> | D | 跳到页面头部 |
| Navigation | <nav> | D | 跳到导航区域 |
| Main | <main> | D | 跳到主内容 |
| Complementary | <aside> | D | 跳到辅助内容 |
| Content Info | <footer> | D | 跳到页脚 |
| Search | <search> | D | 跳到搜索区域 |
| Region | <section> | D | 跳到有标题的区域 |
语义化标签的屏幕阅读器播报
html
<!-- 不同标签的屏幕阅读器播报对比 -->
<!-- div:无播报 -->
<div>内容</div>
<!-- 屏幕阅读器:内容(没有区域标识) -->
<!-- header(页面级):landmark -->
<header>内容</header>
<!-- 屏幕阅读器:banner landmark, 内容 -->
<!-- nav:landmark -->
<nav aria-label="主导航">链接</nav>
<!-- 屏幕阅读器:navigation landmark "主导航", 链接列表 -->
<!-- main:landmark -->
<main>内容</main>
<!-- 屏幕阅读器:main landmark, 内容 -->
<!-- article -->
<article>
<h2>文章标题</h2>
<p>正文</p>
</article>
<!-- 屏幕阅读器:article landmark, "文章标题", heading level 2, 正文 -->
<!-- section(有标题) -->
<section>
<h2>章节标题</h2>
<p>内容</p>
</section>
<!-- 屏幕阅读器:region "章节标题", heading level 2, 内容 -->
<!-- details -->
<details>
<summary>点击展开</summary>
<p>隐藏内容</p>
</details>
<!-- 屏幕阅读器:点击展开, disclosure triangle, 折叠状态 -->详细说明
常见的无障碍问题
1. 缺少 alt 属性
html
<!-- 错误:图片没有 alt -->
<img src="chart.png">
<!-- 屏幕阅读器播报:[image] 或读出文件名 -->
<!-- 正确:有描述性 alt -->
<img src="chart.png" alt="2024 年前端框架使用率柱状图">
<!-- 装饰性图片 -->
<img src="decorative.png" alt="" role="presentation">2. 缺少表单标签
html
<!-- 错误:没有 label -->
<input type="text" placeholder="姓名">
<!-- 屏幕阅读器播报:edit text, 姓名(只读 placeholder) -->
<!-- 正确:有 label -->
<label for="name">姓名</label>
<input type="id="name" type="text">
<!-- 屏幕阅读器播报:姓名, edit text -->3. 按钮语义不正确
html
<!-- 错误:div 模拟按钮 -->
<div class="button" onclick="submit()">提交</div>
<!-- 屏幕阅读器播报:group, 提交(无法用 Enter 激活) -->
<!-- 正确:使用 button -->
<button type="submit">提交</button>
<!-- 屏幕阅读器播报:button, 提交(可用 Enter/Space 激活) -->4. 链接文本不明确
html
<!-- 错误 -->
<a href="/article">点击这里</a>
<a href="/article">更多</a>
<!-- 正确 -->
<a href="/article">阅读 HTML5 语义化教程</a>
<a href="/article">查看更多 CSS Grid 示例</a>语义化标签的可访问性增强
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>无障碍示例</title>
<style>
/* Skip link */
.skip-link {
position: absolute; top: -100%; left: 0;
background: #0078d7; color: #fff;
padding: 0.75rem 1.5rem; z-index: 10000;
text-decoration: none;
}
.skip-link:focus { top: 0; }
body { font-family: -apple-system, sans-serif; }
.site-header { background: #2c3e50; color: #fff; padding: 1rem 2rem; }
main { max-width: 800px; margin: 2rem auto; padding: 0 2rem; }
.sr-only {
position: absolute; width: 1px; height: 1px;
padding: 0; margin: -1px; overflow: hidden;
clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
</style>
</head>
<body>
<!-- Skip link:键盘用户可以跳过导航 -->
<a class="skip-link" href="#main-content">跳转到主要内容</a>
<header class="site-header">
<h1>TechBlog</h1>
<!-- nav 需要 aria-label 区分 -->
<nav aria-label="主导航">
<ul>
<li><a href="/" aria-current="page">首页</a></li>
<li><a href="/blog">博客</a></li>
<li><a href="/about">关于</a></li>
</ul>
</nav>
</header>
<main id="main-content">
<article>
<h1>CSS Grid 布局教程</h1>
<p>Grid 是二维布局系统...</p>
<!-- 图片有 alt -->
<figure>
<img src="grid-layout.png" alt="CSS Grid 网格布局示意图">
<figcaption>图1:Grid 网格布局</figcaption>
</figure>
<!-- 表单有 label -->
<form>
<div>
<label for="email">邮箱地址</label>
<input type="email" id="email" name="email" required aria-describedby="email-help">
<span id="email-help" class="sr-only">请输入您的邮箱地址</span>
</div>
<div>
<label for="subscribe">订阅类型</label>
<select id="subscribe" name="type">
<option value="daily">每日</option>
<option value="weekly">每周</option>
</select>
</div>
<button type="submit">订阅</button>
</form>
</article>
</main>
<footer>
<p>© 2024 TechBlog</p>
<nav aria-label="页脚导航">
<a href="/privacy">隐私政策</a>
</nav>
</footer>
</body>
</html>实战示例
可访问的导航组件
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>可访问导航示例</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, sans-serif; }
.site-header { background: #1e293b; color: #fff; padding: 1rem 2rem; }
.main-nav ul {
list-style: none; display: flex; gap: 0;
}
.main-nav li { position: relative; }
.main-nav > ul > li > a {
display: block; padding: 0.75rem 1.25rem;
color: #cbd5e0; text-decoration: none;
}
.main-nav > ul > li > a:hover,
.main-nav > ul > li > a[aria-expanded="true"] {
background: rgba(255,255,255,0.1); color: #fff;
}
/* 下拉菜单 */
.dropdown-menu {
display: none; position: absolute; top: 100%; left: 0;
background: #fff; min-width: 200px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
border-radius: 0 0 8px 8px; z-index: 100;
}
.dropdown-menu[aria-hidden="false"] { display: block; }
.dropdown-menu a {
display: block; padding: 0.75rem 1.25rem;
color: #334155; text-decoration: none;
}
.dropdown-menu a:hover { background: #f1f5f9; }
main { max-width: 800px; margin: 2rem auto; padding: 0 2rem; }
</style>
</head>
<body>
<header class="site-header">
<h1>TechSite</h1>
<nav class="main-nav" aria-label="主导航">
<ul>
<li><a href="/" aria-current="page">首页</a></li>
<li>
<a href="#" aria-haspopup="true" aria-expanded="false" role="button">
教程
</a>
<!-- 下拉菜单 -->
<ul class="dropdown-menu" role="menu" aria-hidden="true">
<li role="none"><a href="/html" role="menuitem">HTML</a></li>
<li role="none"><a href="/css" role="menuitem">CSS</a></li>
<li role="none"><a href="/js" role="menuitem">JavaScript</a></li>
</ul>
</li>
<li><a href="/blog">博客</a></li>
<li><a href="/about">关于</a></li>
</ul>
</nav>
</header>
<main>
<h2>首页</h2>
<p>欢迎来到 TechSite。</p>
</main>
<script>
// 简化的下拉菜单交互(完整实现需要更多逻辑)
const triggers = document.querySelectorAll('[aria-haspopup="true"]');
triggers.forEach(trigger => {
trigger.addEventListener('click', () => {
const expanded = trigger.getAttribute('aria-expanded') === 'true';
const menu = trigger.nextElementSibling;
trigger.setAttribute('aria-expanded', !expanded);
menu.setAttribute('aria-hidden', expanded);
});
});
</script>
</body>
</html>注意事项
测试工具
| 工具 | 类型 | 说明 |
|---|---|---|
| Lighthouse | Chrome 扩展 | 综合无障碍评分 |
| axe DevTools | 浏览器扩展 | 详细无障碍检查 |
| NVDA | 屏幕阅读器 | Windows 免费屏幕阅读器 |
| VoiceOver | 屏幕阅读器 | macOS/iOS 内置 |
| WAVE | 在线工具 | 可访问性评估 |
最佳实践
- 使用 skip-to-content 链接:为键盘用户提供跳过导航的途径
- 为 nav 添加 aria-label:帮助区分多个导航区域
- 使用 aria-current:标记当前页面/当前步骤
- 使用语义化按钮:用
<button>而非<div> - 测试无障碍性:使用屏幕阅读器测试页面
- 添加 alt 文本:为所有有意义的图片添加描述
下一节
继续学习:大纲算法