Web无障碍(Accessibility,简称A11y)确保所有用户,包括残障人士,都能平等地访问和使用网站内容。这不仅是一项社会责任,也是法律要求,更能提升所有用户的使用体验。本文将介绍Web无障碍的核心原则和实施方法。

1. 无障碍基础概念

WCAG原则

WCAG (Web Content Accessibility Guidelines) 提出了四个核心原则:

1. 感知性 (Perceivable)

  • 信息和用户界面组件必须以用户可以感知的方式呈现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!-- ✅ 提供替代文本 -->
<img src="chart.png" alt="2024年销售数据图表,显示Q1增长15%,Q2增长20%">

<!-- ✅ 为视频提供字幕 -->
<video controls>
    <source src="demo.mp4" type="video/mp4">
    <track kind="captions" src="demo-captions.vtt" srclang="zh" label="中文字幕">
</video>

<!-- ❌ 缺少替代文本 -->
<img src="chart.png">

2. 可操作性 (Operable)

  • 用户界面组件和导航必须是可操作的
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!-- ✅ 键盘可访问 -->
<button onclick="toggleMenu()" onkeydown="handleKeydown(event)">
    切换菜单
</button>

<!-- ✅ 足够大的点击目标 -->
<a href="/contact" class="large-link">联系我们</a>

<style>
.large-link {
    display: inline-block;
    padding: 12px 24px;
    min-height: 44px;
    min-width: 44px;
}
</style>

3. 可理解性 (Understandable)

  • 信息和用户界面的操作必须是可理解的
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!-- ✅ 清晰的表单标签 -->
<label for="email">邮箱地址</label>
<input type="email" id="email" name="email" required aria-required="true">

<!-- ✅ 错误提示明确 -->
<div class="error" role="alert">
    邮箱地址格式不正确,请重新输入
</div>

<!-- ✅ 提供帮助信息 -->
<div class="help-text" id="password-help">
    密码必须包含至少8个字符,包括大小写字母和数字
</div>
<input type="password" id="password" aria-describedby="password-help">

4. 健壮性 (Robust)

  • 内容必须足够健壮,能够被各种用户代理(包括辅助技术)可靠地解析
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<!-- ✅ 使用语义化HTML -->
<main>
    <article>
        <h1>文章标题</h1>
        <p>文章内容...</p>
    </article>
</main>

<!-- ✅ 有效的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>
</head>
<body>
    <!-- 内容 -->
</body>
</html>

2. 语义化HTML

正确的标题层级

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- ✅ 正确的标题层级 -->
<h1>网站主标题</h1>
    <h2>关于我们</h2>
        <h3>公司历史</h3>
        <h3>团队介绍</h3>
    <h2>产品服务</h2>
        <h3>产品列表</h3>
        <h3>价格方案</h3>
    <h2>联系方式</h2>

<!-- ❌ 跳级标题 -->
<h1>网站主标题</h1>
    <h3>关于我们</h3>  <!-- 跳过了h2 -->
    <h4>公司历史</h4>

<!-- 标题导航工具 -->
<nav class="skip-links" aria-label="页面导航">
    <h2 class="sr-only">页面导航</h2>
    <ul>
        <li><a href="#main">跳转到主内容</a></li>
        <li><a href="#navigation">跳转到导航</a></li>
        <li><a href="#search">跳转到搜索</a></li>
    </ul>
</nav>

列表和导航

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!-- ✅ 语义化导航 -->
<nav role="navigation" aria-label="主导航">
    <ul>
        <li><a href="/" aria-current="page">首页</a></li>
        <li><a href="/about">关于我们</a></li>
        <li><a href="/products">产品</a></li>
        <li><a href="/contact">联系我们</a></li>
    </ul>
</nav>

<!-- ✅ 面包屑导航 -->
<nav aria-label="面包屑导航">
    <ol>
        <li><a href="/">首页</a></li>
        <li><a href="/products">产品</a></li>
        <li aria-current="page">产品详情</li>
    </ol>
</nav>

<!-- ✅ 定义列表 -->
<dl>
    <dt>HTML</dt>
    <dd>超文本标记语言,用于创建网页的标准标记语言</dd>
    <dt>CSS</dt>
    <dd>层叠样式表,用于设置网页的视觉样式</dd>
    <dt>JavaScript</dt>
    <dd>脚本语言,用于实现网页的交互功能</dd>
</dl>

表单语义化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<!-- ✅ 完整的表单结构 -->
<form action="/submit" method="post" novalidate>
    <fieldset>
        <legend>用户信息</legend>

        <div class="form-group">
            <label for="name">
                姓名 <span aria-label="必填项">*</span>
            </label>
            <input
                type="text"
                id="name"
                name="name"
                required
                aria-required="true"
                aria-describedby="name-help"
                autocomplete="name"
            >
            <div id="name-help" class="help-text">
                请输入您的真实姓名
            </div>
        </div>

        <div class="form-group">
            <label for="email">邮箱地址</label>
            <input
                type="email"
                id="email"
                name="email"
                required
                aria-required="true"
                autocomplete="email"
            >
        </div>

        <fieldset>
            <legend>性别</legend>
            <div class="radio-group">
                <input type="radio" id="male" name="gender" value="male">
                <label for="male">男性</label>

                <input type="radio" id="female" name="gender" value="female">
                <label for="female">女性</label>

                <input type="radio" id="other" name="gender" value="other">
                <label for="other">其他</label>
            </div>
        </fieldset>
    </fieldset>

    <div class="form-actions">
        <button type="submit">提交</button>
        <button type="reset">重置</button>
    </div>
</form>

3. ARIA属性使用

ARIA角色

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!-- ✅ 标识地标区域 -->
<header role="banner">
    <h1>网站标题</h1>
</header>

<nav role="navigation" aria-label="主导航">
    <!-- 导航内容 -->
</nav>

<main role="main">
    <!-- 主要内容 -->
</main>

<aside role="complementary" aria-label="侧边栏">
    <!-- 补充内容 -->
</aside>

<footer role="contentinfo">
    <!-- 版权信息 -->
</footer>

<!-- ✅ 动态内容区域 -->
<div role="region" aria-live="polite" aria-label="状态消息">
    <p id="status-message">操作成功完成</p>
</div>

<div role="alert" aria-live="assertive">
    <p>重要:您的会话即将过期</p>
</div>

ARIA状态和属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<!-- ✅ 按钮状态 -->
<button
    aria-pressed="false"
    aria-label="切换静音"
    onclick="toggleMute(this)"
>
    <span aria-hidden="true">🔊</span>
</button>

<!-- ✅ 进度指示器 -->
<div
    role="progressbar"
    aria-valuenow="75"
    aria-valuemin="0"
    aria-valuemax="100"
    aria-label="文件上传进度"
>
    75%
</div>

<!-- ✅ 选项卡界面 -->
<div role="tablist" aria-label="产品选项卡">
    <button
        role="tab"
        aria-selected="true"
        aria-controls="panel1"
        id="tab1"
    >
        产品详情
    </button>
    <button
        role="tab"
        aria-selected="false"
        aria-controls="panel2"
        id="tab2"
    >
        用户评价
    </button>
</div>

<div
    role="tabpanel"
    id="panel1"
    aria-labelledby="tab1"
    tabindex="0"
>
    <!-- 产品详情内容 -->
</div>

<div
    role="tabpanel"
    id="panel2"
    aria-labelledby="tab2"
    hidden
    tabindex="0"
>
    <!-- 用户评价内容 -->
</div>

4. 键盘导航

焦点管理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<!-- ✅ 自定义可聚焦元素 -->
<div
    tabindex="0"
    role="button"
    onclick="handleClick()"
    onkeydown="handleKeydown(event)"
    aria-label="自定义按钮"
>
    点击我
</div>

<script>
function handleKeydown(event) {
    if (event.key === 'Enter' || event.key === ' ') {
        event.preventDefault();
        handleClick();
    }
}
</script>

<!-- ✅ 模态框焦点管理 -->
<div id="modal" class="modal" role="dialog" aria-labelledby="modal-title" hidden>
    <div class="modal-content">
        <h2 id="modal-title">对话框标题</h2>
        <button onclick="closeModal()" aria-label="关闭对话框">×</button>
        <p>对话框内容</p>
        <button onclick="confirmModal()">确认</button>
    </div>
</div>

<script>
let previousFocusElement;

function openModal() {
    const modal = document.getElementById('modal');
    previousFocusElement = document.activeElement;

    modal.hidden = false;
    modal.setAttribute('aria-hidden', 'false');

    // 焦点移到模态框内第一个可聚焦元素
    modal.querySelector('button').focus();

    // 限制Tab键在模态框内循环
    modal.addEventListener('keydown', trapFocus);
}

function closeModal() {
    const modal = document.getElementById('modal');
    modal.hidden = true;
    modal.setAttribute('aria-hidden', 'true');
    modal.removeEventListener('keydown', trapFocus);

    // 恢复之前的焦点
    if (previousFocusElement) {
        previousFocusElement.focus();
    }
}

function trapFocus(event) {
    if (event.key === 'Tab') {
        const focusableElements = event.currentTarget.querySelectorAll(
            'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
        );

        const firstElement = focusableElements[0];
        const lastElement = focusableElements[focusableElements.length - 1];

        if (event.shiftKey) {
            if (document.activeElement === firstElement) {
                event.preventDefault();
                lastElement.focus();
            }
        } else {
            if (document.activeElement === lastElement) {
                event.preventDefault();
                firstElement.focus();
            }
        }
    }
}
</script>

跳过链接

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!-- ✅ 跳过链接(对屏幕阅读器用户重要) -->
<a href="#main-content" class="skip-link">
    跳转到主内容
</a>

<style>
.skip-link {
    position: absolute;
    top: -40px;
    left: 6px;
    background: #000;
    color: #fff;
    padding: 8px;
    text-decoration: none;
    border-radius: 4px;
    z-index: 1000;
}

.skip-link:focus {
    top: 6px;
}
</style>

<main id="main-content">
    <!-- 主要内容 -->
</main>

5. 色彩和对比度

对比度要求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* ✅ 足够的对比度(至少4.5:1) */
.text-high-contrast {
    color: #000000;  /* 黑色文字 */
    background-color: #ffffff;  /* 白色背景 */
}

.text-medium-contrast {
    color: #333333;  /* 深灰色文字 */
    background-color: #ffffff;  /* 白色背景 */
}

/* ✅ 大文本对比度要求(至少3:1) */
.heading-large {
    font-size: 24px;
    color: #666666;  /* 可以使用较低的对比度 */
    background-color: #ffffff;
}

/* ❌ 对比度不足 */
.poor-contrast {
    color: #cccccc;  /* 浅灰色文字 */
    background-color: #ffffff;  /* 白色背景 */
}

色彩无障碍

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<!-- ✅ 不依赖颜色传达信息 -->
<div class="status-indicators">
    <div class="indicator success">
        <span class="icon">✓</span>
        <span class="text">操作成功</span>
    </div>
    <div class="indicator error">
        <span class="icon">✗</span>
        <span class="text">操作失败</span>
    </div>
</div>

<style>
.indicator {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px;
    border-radius: 4px;
}

.success {
    background-color: #d4edda;
    border: 1px solid #c3e6cb;
    color: #155724;
}

.error {
    background-color: #f8d7da;
    border: 1px solid #f5c6cb;
    color: #721c24;
}

/* ✅ 高对比度模式支持 */
@media (prefers-contrast: high) {
    .success {
        background-color: #000;
        color: #fff;
        border: 2px solid #fff;
    }

    .error {
        background-color: #000;
        color: #fff;
        border: 2px solid #fff;
    }
}

/* ✅ 用户偏好支持 */
@media (prefers-reduced-motion: reduce) {
    * {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

@media (prefers-color-scheme: dark) {
    body {
        background-color: #121212;
        color: #ffffff;
    }

    .card {
        background-color: #1e1e1e;
        color: #ffffff;
    }
}

6. 多媒体无障碍

图片描述

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!-- ✅ 装饰性图片 -->
<img src="decorative-border.png" alt="" role="presentation">

<!-- ✅ 信息性图片 -->
<img src="company-logo.png" alt="ABC公司">

<!-- ✅ 复杂图片的详细描述 -->
<img src="complex-chart.png"
     alt="销售趋势图显示2024年各季度销售数据"
     longdesc="/charts/description.html">

<!-- ✅ 图片地图 -->
<img src="workspace.jpg" alt="工作区域平面图" usemap="#workspacemap">
<map name="workspacemap">
    <area shape="rect" coords="0,0,50,50" alt="会议室A" href="/room-a">
    <area shape="rect" coords="60,0,110,50" alt="会议室B" href="/room-b">
</map>

视频无障碍

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<!-- ✅ 完整的视频无障碍支持 -->
<video
    controls
    width="640"
    height="360"
    poster="video-poster.jpg"
>
    <!-- 多种视频格式支持 -->
    <source src="video.mp4" type="video/mp4">
    <source src="video.webm" type="video/webm">

    <!-- 字幕轨道 -->
    <track
        kind="subtitles"
        src="subtitles-zh.vtt"
        srclang="zh"
        label="中文字幕"
    >
    <track
        kind="subtitles"
        src="subtitles-en.vtt"
        srclang="en"
        label="English Subtitles"
    >

    <!-- 描述音轨 -->
    <track
        kind="descriptions"
        src="descriptions.vtt"
        srclang="zh"
        label="音频描述"
    >

    <!-- 章节标记 -->
    <track
        kind="chapters"
        src="chapters.vtt"
        srclang="zh"
        label="章节"
    >

    <!-- 不支持视频的替代内容 -->
    <div class="video-fallback">
        <p>您的浏览器不支持视频播放。</p>
        <p><a href="video.mp4">下载视频文件</a></p>
        <div class="video-transcript">
            <h3>视频文字记录</h3>
            <p>这里是视频的完整文字记录...</p>
        </div>
    </div>
</video>

7. JavaScript无障碍

事件处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// ✅ 无障碍的事件处理
class AccessibleButton {
    constructor(element) {
        this.element = element;
        this.init();
    }

    init() {
        // 设置ARIA属性
        this.element.setAttribute('role', 'button');
        this.element.setAttribute('tabindex', '0');

        // 添加事件监听器
        this.element.addEventListener('click', this.handleClick.bind(this));
        this.element.addEventListener('keydown', this.handleKeydown.bind(this));
    }

    handleClick(event) {
        event.preventDefault();
        this.activate();
    }

    handleKeydown(event) {
        // 支持Enter和空格键
        if (event.key === 'Enter' || event.key === ' ') {
            event.preventDefault();
            this.activate();
        }
    }

    activate() {
        // 执行按钮动作
        console.log('按钮被激活');

        // 提供反馈给屏幕阅读器
        this.announceToScreenReader('操作已执行');
    }

    announceToScreenReader(message) {
        // 创建临时通知元素
        const announcement = document.createElement('div');
        announcement.setAttribute('role', 'status');
        announcement.setAttribute('aria-live', 'polite');
        announcement.className = 'sr-only';
        announcement.textContent = message;

        document.body.appendChild(announcement);

        // 清理通知元素
        setTimeout(() => {
            document.body.removeChild(announcement);
        }, 1000);
    }
}

// 屏幕阅读器专用CSS
const style = document.createElement('style');
style.textContent = `
    .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;
    }
`;
document.head.appendChild(style);

// 使用示例
const customButton = document.getElementById('custom-button');
new AccessibleButton(customButton);

动态内容更新

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// ✅ 无障碍的动态内容更新
class AccessibleContent {
    constructor() {
        this.liveRegion = document.getElementById('live-region');
        this.statusRegion = document.getElementById('status-region');
    }

    // 更新内容并通知屏幕阅读器
    updateContent(content, type = 'polite') {
        const region = type === 'assertive' ? this.statusRegion : this.liveRegion;

        // 更新内容
        region.textContent = content;

        // 确保内容被读取
        setTimeout(() => {
            region.textContent = '';
            region.textContent = content;
        }, 100);
    }

    // 重要通知(打断当前阅读)
    announceImportant(message) {
        this.updateContent(message, 'assertive');
    }

    // 一般通知(不打断)
    announcePolite(message) {
        this.updateContent(message, 'polite');
    }
}

// 使用示例
const accessibleContent = new AccessibleContent();

// AJAX请求完成后的通知
fetch('/api/data')
    .then(response => response.json())
    .then(data => {
        // 更新页面内容
        document.getElementById('content').innerHTML = data.html;

        // 通知屏幕阅读器
        accessibleContent.announcePolite('内容已更新');
    })
    .catch(error => {
        accessibleContent.announceImportant('加载失败,请重试');
    });

8. 移动端无障碍

触摸目标大小

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* ✅ 足够大的触摸目标 */
.touch-target {
    min-height: 44px;
    min-width: 44px;
    padding: 12px;
    margin: 8px;
}

/* ✅ 按钮间距 */
.button-group button {
    margin: 8px;
    min-height: 44px;
    min-width: 44px;
}

/* ✅ 输入框尺寸 */
input, textarea, select {
    min-height: 44px;
    font-size: 16px; /* 防止iOS缩放 */
    padding: 12px;
    margin: 8px 0;
}

移动端导航

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!-- ✅ 移动端友好导航 -->
<nav class="mobile-nav" role="navigation" aria-label="移动端导航">
    <button
        class="nav-toggle"
        aria-expanded="false"
        aria-controls="nav-menu"
        onclick="toggleNavigation()"
    >
        <span class="hamburger-icon"></span>
        <span class="sr-only">切换菜单</span>
    </button>

    <ul id="nav-menu" class="nav-menu" hidden>
        <li><a href="/">首页</a></li>
        <li><a href="/about">关于</a></li>
        <li><a href="/contact">联系</a></li>
    </ul>
</nav>

<script>
function toggleNavigation() {
    const menu = document.getElementById('nav-menu');
    const toggle = document.querySelector('.nav-toggle');
    const isExpanded = menu.hidden === false;

    menu.hidden = isExpanded;
    toggle.setAttribute('aria-expanded', !isExpanded);

    // 如果菜单打开,焦点移到第一个链接
    if (!isExpanded) {
        menu.querySelector('a').focus();
    }
}
</script>

9. 测试和验证

自动化测试工具

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ✅ 使用axe-core进行无障碍测试
const axe = require('axe-core');

async function testAccessibility() {
    const results = await axe.run(document);

    if (results.violations.length === 0) {
        console.log('✅ 无障碍测试通过');
        return true;
    }

    console.log('❌ 发现无障碍问题:');
    results.violations.forEach(violation => {
        console.log(`- ${violation.description}`);
        console.log(`  影响: ${violation.impact}`);
        console.log(`  元素: ${violation.nodes.map(node => node.target).join(', ')}`);
    });

    return false;
}

// 在页面加载后运行测试
document.addEventListener('DOMContentLoaded', testAccessibility);

手动测试清单

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!-- 无障碍测试清单 -->
<div class="accessibility-checklist">
    <h2>无障碍测试清单</h2>

    <fieldset>
        <legend>键盘导航测试</legend>
        <ul>
            <li>
                <label>
                    <input type="checkbox"> 可以使用Tab键导航到所有交互元素
                </label>
            </li>
            <li>
                <label>
                    <input type="checkbox"> 焦点顺序逻辑清晰
                </label>
            </li>
            <li>
                <label>
                    <input type="checkbox"> 可以使用Enter/空格键激活按钮和链接
                </label>
            </li>
        </ul>
    </fieldset>

    <fieldset>
        <legend>屏幕阅读器测试</legend>
        <ul>
            <li>
                <label>
                    <input type="checkbox"> 图片有合适的替代文本
                </label>
            </li>
            <li>
                <label>
                    <input type="checkbox"> 表单字段有明确的标签
                </label>
            </li>
            <li>
                <label>
                    <input type="checkbox"> 动态内容变化时有通知
                </label>
            </li>
        </ul>
    </fieldset>
</div>

10. 无障碍最佳实践

开发流程集成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// webpack配置中的无障碍检查
const ESLintPlugin = require('eslint-webpack-plugin');

module.exports = {
    plugins: [
        new ESLintPlugin({
            extensions: ['js', 'jsx'],
            failOnError: true,
            eslintOptions: {
                extends: ['plugin:jsx-a11y/recommended'],
                rules: {
                    'jsx-a11y/alt-text': 'error',
                    'jsx-a11y/anchor-is-valid': 'error',
                    'jsx-a11y/click-events-have-key-events': 'error'
                }
            }
        })
    ]
};

// CI/CD中的无障碍测试
const { execSync } = require('child_process');

function runAccessibilityTests() {
    try {
        // 使用pa11y进行自动化测试
        execSync('pa11y http://localhost:3000', { stdio: 'inherit' });
        console.log('✅ 无障碍测试通过');
    } catch (error) {
        console.error('❌ 无障碍测试失败');
        process.exit(1);
    }
}

// 在CI/CD管道中运行
if (process.env.CI) {
    runAccessibilityTests();
}

团队培训

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 无障碍开发指南

## 核心原则
1. **语义化HTML优先**:使用正确的HTML元素表达内容结构
2. **键盘可访问**:确保所有功能都可以通过键盘操作
3. **颜色不是唯一标识**:不要仅依赖颜色传达信息
4. **提供替代文本**:为非文本内容提供文字描述

## 开发检查点
- [ ] 所有交互元素都有键盘支持
- [ ] 图片都有alt属性
- [ ] 表单都有标签
- [ ] 色彩对比度符合标准
- [ ] 使用ARIA增强可访问性

总结

Web无障碍是每个开发者都应该重视的责任。通过实施这些最佳实践,我们能够:

提升用户体验:

  • 更好的SEO排名
  • 更广泛的用户覆盖
  • 提升所有用户的可用性

履行社会责任:

  • 确保数字包容性
  • 符合法律法规要求
  • 体现技术道德

长期收益:

  • 减少维护成本
  • 提高代码质量
  • 建立良好的技术品牌

记住,无障碍不是锦上添花,而是基本要求。从项目开始就考虑无障碍,会比后期修复成本低得多。让我们一起构建人人可访问的Web世界!