Web可访问性概述
Web可访问性(Web Accessibility)是指网站、工具和技术能够被残障人士使用的程度。可访问性涵盖了所有影响访问Web内容的障碍,包括视觉、听觉、物理、言语、认知和神经性障碍。
提示: 可访问性不仅帮助残障人士,也帮助有暂时性损伤的人(如手臂骨折)和情境限制的人(如在强光下使用手机)。
可访问性的重要性
- 道德责任: 确保每个人都能访问信息和服务
- 法律要求: 许多国家和地区有可访问性相关法律
- 商业利益: 扩大用户群体,改善用户体验
- SEO好处: 可访问性实践通常与SEO最佳实践重叠
- 创新驱动: 可访问性设计往往带来更好的整体设计
可访问性法规和标准
全球范围内有多种可访问性法规和标准,包括:
- WCAG(Web内容可访问性指南): 国际公认的Web可访问性标准
- Section 508: 美国联邦机构必须遵循的可访问性标准
- EN 301 549: 欧洲可访问性标准
- AODA: 加拿大安大略省的可访问性法案
WCAG指南
Web内容可访问性指南(WCAG)是国际公认的Web可访问性标准:
四项原则(POUR)
| 原则 | 描述 | 关键要点 |
|---|---|---|
| 可感知 | 信息和用户界面组件必须以用户可以感知的方式呈现 | 提供文本替代、时间基媒体替代、适应性强、可区分 |
| 可操作 | 用户界面组件和导航必须可操作 | 键盘可访问、足够的时间、不会引起癫痫、易于导航 |
| 可理解 | 信息和用户界面的操作必须可理解 | 可读性、可预测性、输入辅助 |
| 健壮性 | 内容必须足够健壮,能够被各种用户代理可靠地解释 | 兼容性 |
一致性等级
- A级: 最基本级别的可访问性
- AA级: 解决大多数可访问性问题的级别,许多法律要求此级别
- AAA级: 最高级别的可访问性
WCAG 2.1和2.2新增内容
WCAG 2.1和2.2版本增加了对移动设备、低视力用户和认知障碍用户的支持:
- 移动可访问性: 手势操作、指针取消、目标尺寸等
- 认知和学习的可访问性: 识别目的、一致导航等
- 低视力的可访问性: 文本间距、内容悬停等
ARIA(可访问的富互联网应用程序)
ARIA是一组属性,用于增强HTML的可访问性,特别是在动态内容和复杂UI组件中。
主要ARIA属性
| 属性 | 描述 | 示例 |
|---|---|---|
role |
定义元素的角色 | role="navigation" |
aria-label |
为元素提供标签 | aria-label="关闭菜单" |
aria-labelledby |
引用其他元素作为标签 | aria-labelledby="title1" |
aria-describedby |
引用其他元素作为描述 | aria-describedby="help-text" |
aria-hidden |
对辅助技术隐藏元素 | aria-hidden="true" |
aria-expanded |
指示可折叠元素的展开状态 | aria-expanded="false" |
aria-required |
指示输入字段是必需的 | aria-required="true" |
aria-live |
指示动态内容区域 | aria-live="polite" |
ARIA使用示例
ARIA示例
<!-- 导航区域 -->
<nav aria-label="主导航">
<ul>
<li><a href="#">首页</a></li>
<li><a href="#">关于</a></li>
</ul>
</nav>
<!-- 对话框 -->
<div role="dialog" aria-labelledby="dialog-title" aria-describedby="dialog-desc">
<h2 id="dialog-title">确认删除</h2>
<p id="dialog-desc">您确定要删除此项吗?此操作不可撤销。</p>
<button aria-label="确认删除">删除</button>
<button aria-label="取消删除">取消</button>
</div>
<!-- 进度指示器 -->
<div role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100">
75%
</div>
<!-- 动态内容区域 -->
<div aria-live="polite" aria-atomic="true">
动态更新的内容将在这里宣布
</div>
注意: 只有在必要时才使用ARIA。首先使用原生HTML元素,因为它们已经内置了可访问性特性。
ARIA角色分类
ARIA角色分为以下几类:
- 抽象角色: 不应直接使用的角色,仅用于角色继承
- 小部件角色: 用于交互式UI元素的角色,如按钮、滑块等
- 文档结构角色: 描述页面结构的角色,如文章、横幅等
- 地标角色: 标识页面重要区域的角色,如导航、主要等
键盘可访问性
确保所有功能都可以通过键盘访问:
Tab键导航
- 确保所有交互元素(链接、按钮、表单控件)可以通过Tab键访问
- 使用
tabindex属性控制Tab键顺序 - 避免使用
tabindex值大于0 - 使用
tabindex="-1"从Tab键顺序中移除元素,但仍允许编程聚焦
键盘操作
键盘操作示例
<!-- 可折叠内容 -->
<button aria-expanded="false" aria-controls="collapsible-content">
显示更多
</button>
<div id="collapsible-content" hidden>
可折叠的内容...
</div>
<script>
document.querySelector('button').addEventListener('click', function() {
const expanded = this.getAttribute('aria-expanded') === 'true';
this.setAttribute('aria-expanded', !expanded);
document.getElementById('collapsible-content').hidden = expanded;
});
// 添加键盘支持
document.querySelector('button').addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.click();
}
});
</script>
跳过链接
跳过链接
<!-- 跳过导航链接 -->
<a href="#main-content" class="skip-link">跳到主内容</a>
<header>
<!-- 导航菜单 -->
</header>
<main id="main-content" tabindex="-1">
<!-- 主内容 -->
</main>
<style>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
</style>
键盘快捷键
为复杂交互提供键盘快捷键:
- Enter/空格键: 激活按钮或链接
- Tab键: 在可聚焦元素间移动
- 箭头键: 在选项间导航
- Esc键: 关闭对话框或弹出窗口
图像和多媒体可访问性
图像替代文本
alt文本最佳实践
<!-- 信息性图像 -->
<img src="chart.png" alt="2026年销售图表,显示第一季度增长20%">
<!-- 装饰性图像 -->
<img src="divider.png" alt="">
<!-- 链接中的图像 -->
<a href="/about">
<img src="logo.png" alt="公司首页 - 关于我们">
</a>
<!-- 复杂图像的长描述 -->
<img src="infographic.png" alt="气候变化影响信息图" longdesc="climate-desc.html">
<a href="climate-desc.html">信息图详细描述</a>
多媒体可访问性
音频和视频可访问性
<!-- 视频字幕和描述 -->
<video controls>
<source src="video.mp4" type="video/mp4">
<track kind="captions" src="captions.vtt" srclang="zh" label="中文">
<track kind="descriptions" src="descriptions.vtt" srclang="zh">
</video>
<!-- 音频转录 -->
<audio controls>
<source src="speech.mp3" type="audio/mpeg">
</audio>
<a href="transcript.html">查看文字转录</a>
SVG可访问性
确保SVG图形的可访问性:
SVG可访问性
<svg role="img" aria-labelledby="svg-title svg-desc">
<title id="svg-title">公司增长图表</title>
<desc id="svg-desc">
显示过去五年公司收入稳步增长的折线图,
从2022年的100万元增长到2026年的500万元
</desc>
<!-- SVG内容 -->
</svg>
表单可访问性
标签和指令
表单标签
<!-- 显式标签 -->
<label for="username">用户名:</label>
<input type="text" id="username" name="username">
<!-- 隐式标签 -->
<label>
邮箱:
<input type="email" name="email">
</label>
<!-- 使用aria-labelledby -->
<span id="phone-label">电话号码:</span>
<input type="tel" aria-labelledby="phone-label">
<!-- 使用aria-describedby提供额外信息 -->
<label for="password">密码:</label>
<input type="password" id="password" name="password"
aria-describedby="password-help">
<div id="password-help">密码必须包含至少8个字符</div>
错误处理
表单错误
<form>
<label for="email">邮箱:</label>
<input type="email" id="email" name="email"
aria-invalid="false"
aria-describedby="email-error">
<div id="email-error" role="alert"></div>
<button type="submit">提交</button>
</form>
<script>
document.querySelector('form').addEventListener('submit', function(e) {
e.preventDefault();
const email = document.getElementById('email');
const error = document.getElementById('email-error');
if (!email.value.includes('@')) {
email.setAttribute('aria-invalid', 'true');
error.textContent = '请输入有效的邮箱地址';
email.focus();
} else {
email.setAttribute('aria-invalid', 'false');
error.textContent = '';
// 提交表单
}
});
</script>
表单分组
使用fieldset和legend对相关表单控件进行分组:
表单分组
<fieldset>
<legend>联系信息</legend>
<label for="name">姓名:</label>
<input type="text" id="name" name="name">
<label for="email">邮箱:</label>
<input type="email" id="email" name="email">
</fieldset>
<fieldset>
<legend>订阅偏好</legend>
<input type="checkbox" id="newsletter" name="newsletter">
<label for="newsletter">订阅新闻通讯</label>
<input type="checkbox" id="promotions" name="promotions">
<label for="promotions">接收促销信息</label>
</fieldset>
颜色和对比度
颜色对比度
- 文本和背景之间应有足够的对比度
- 普通文本的对比度至少为4.5:1
- 大文本(18pt以上或14pt粗体)的对比度至少为3:1
- 使用工具检查对比度,如WebAIM颜色对比度检查器
不依赖颜色传达信息
颜色使用
<!-- 不推荐:仅用颜色表示状态 -->
<span style="color: red;">错误</span>
<!-- 推荐:使用颜色和文本 -->
<span style="color: red;" aria-label="错误">
<span class="sr-only">错误:</span>
请输入有效的邮箱地址
</span>
<!-- 推荐:使用图标和文本 -->
<span>
<svg aria-hidden="true">...</svg>
<span class="sr-only">错误:</span>
请输入有效的邮箱地址
</span>
颜色盲友好设计
确保设计对色盲用户友好:
- 避免仅使用颜色区分重要信息
- 使用图案、纹理或文字标签作为补充
- 测试设计在不同类型色盲下的表现
测试和工具
自动化测试工具
- WAVE: Web可访问性评估工具
- axe: 可访问性测试引擎
- Lighthouse: Chrome开发者工具中的可访问性审计
- HTML CodeSniffer: HTML可访问性检查器
手动测试
- 仅使用键盘导航整个网站
- 使用屏幕阅读器测试(如NVDA、JAWS、VoiceOver)
- 检查颜色对比度
- 验证语义结构
- 测试在不同缩放级别下的可用性
屏幕阅读器测试
屏幕阅读器兼容性
<!-- 测试页面结构 -->
<h1>页面主标题</h1>
<nav aria-label="主导航">...</nav>
<main>
<h2>主要内容标题</h2>
<!-- 内容 -->
</main>
<!-- 测试图像替代文本 -->
<img src="logo.png" alt="公司logo">
<!-- 测试表单可访问性 -->
<label for="search">搜索:</label>
<input type="search" id="search" name="search">
<button>搜索</button>
用户测试
与残障用户一起测试网站:
- 招募不同残障类型的用户参与测试
- 观察他们如何使用网站
- 收集反馈并改进设计
移动设备可访问性
触摸目标尺寸
确保触摸目标足够大,便于操作:
- 最小触摸目标尺寸为44x44像素
- 触摸目标间有足够的间距
- 避免触摸目标过小或过于接近
手势操作
确保手势操作可访问:
- 提供替代的单指操作
- 避免复杂的手势操作
- 提供取消手势操作的方法
响应式设计的可访问性
确保响应式设计对所有用户都可访问:
- 在小屏幕上保持足够的对比度
- 确保文本在小屏幕上可读
- 测试在各种设备尺寸下的可访问性
可访问性声明
创建可访问性声明,告知用户网站的可访问性状态:
声明内容
- 网站的可访问性标准遵循情况
- 已知的可访问性问题
- 反馈和联系信息
- 声明的创建和更新日期
声明示例
可访问性声明
<section aria-labelledby="accessibility-statement">
<h2 id="accessibility-statement">可访问性声明</h2>
<p>我们致力于确保所有用户都能访问我们的网站。</p>
<h3>遵循标准</h3>
<p>本网站遵循WCAG 2.1 AA级标准。</p>
<h3>已知问题</h3>
<ul>
<li>某些PDF文档可能不完全可访问</li>
<li>部分视频缺少字幕</li>
</ul>
<h3>反馈</h3>
<p>如果您遇到任何可访问性问题,请<a href="contact.html">联系我们</a>。</p>
</section>
动手练习
创建一个可访问的网页组件:
综合练习
<!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>
.tabs {
margin: 20px 0;
}
.tab-list {
display: flex;
list-style: none;
padding: 0;
margin: 0;
border-bottom: 1px solid #ccc;
}
.tab {
padding: 10px 20px;
border: 1px solid transparent;
border-bottom: none;
background: #f5f5f5;
cursor: pointer;
margin-right: 5px;
border-radius: 5px 5px 0 0;
}
.tab:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
}
.tab[aria-selected="true"] {
background: #fff;
border-color: #ccc;
border-bottom-color: #fff;
margin-bottom: -1px;
}
.tab-panel {
padding: 20px;
border: 1px solid #ccc;
border-top: none;
background: #fff;
}
.tab-panel:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
}
.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>
<h1>可访问的标签页组件</h1>
<div class="tabs">
<div class="tablist" role="tablist" aria-label="示例标签页">
<button class="tab"
role="tab"
aria-selected="true"
aria-controls="panel-1"
id="tab-1">
标签一
</button>
<button class="tab"
role="tab"
aria-selected="false"
aria-controls="panel-2"
id="tab-2"
tabindex="-1">
标签二
</button>
<button class="tab"
role="tab"
aria-selected="false"
aria-controls="panel-3"
id="tab-3"
tabindex="-1">
标签三
</button>
</div>
<div class="tab-panel"
role="tabpanel"
id="panel-1"
aria-labelledby="tab-1"
tabindex="0">
<h2>标签一内容</h2>
<p>这是第一个标签页的内容。</p>
</div>
<div class="tab-panel"
role="tabpanel"
id="panel-2"
aria-labelledby="tab-2"
tabindex="0"
hidden>
<h2>标签二内容</h2>
<p>这是第二个标签页的内容。</p>
</div>
<div class="tab-panel"
role="tabpanel"
id="panel-3"
aria-labelledby="tab-3"
tabindex="0"
hidden>
<h2>标签三内容</h2>
<p>这是第三个标签页的内容。</p>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const tabs = document.querySelectorAll('.tab');
const panels = document.querySelectorAll('.tab-panel');
tabs.forEach(tab => {
tab.addEventListener('click', function() {
// 取消所有标签的选中状态
tabs.forEach(t => {
t.setAttribute('aria-selected', 'false');
t.setAttribute('tabindex', '-1');
});
// 隐藏所有面板
panels.forEach(panel => {
panel.hidden = true;
});
// 激活当前标签
this.setAttribute('aria-selected', 'true');
this.removeAttribute('tabindex');
// 显示对应面板
const panelId = this.getAttribute('aria-controls');
document.getElementById(panelId).hidden = false;
document.getElementById(panelId).focus();
});
// 键盘导航
tab.addEventListener('keydown', function(e) {
const key = e.key;
const currentIndex = Array.from(tabs).indexOf(this);
if (key === 'ArrowRight' || key === 'ArrowLeft') {
// 阻止默认滚动行为
e.preventDefault();
let nextIndex;
if (key === 'ArrowRight') {
nextIndex = (currentIndex + 1) % tabs.length;
} else {
nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
}
tabs[nextIndex].click();
tabs[nextIndex].focus();
}
if (key === 'Home') {
e.preventDefault();
tabs[0].click();
tabs[0].focus();
}
if (key === 'End') {
e.preventDefault();
tabs[tabs.length - 1].click();
tabs[tabs.length - 1].focus();
}
});
});
});
</script>
</body>
</html>