Skip to content

无障碍访问 (a11y)

本文档介绍如何让 VitePress 站点对所有用户友好,包括使用屏幕阅读器、键盘导航和辅助技术的用户。

为什么无障碍很重要?

影响范围

全球约有 15% 的人口有某种形式的残疾,包括:

  • 视觉障碍(盲人、低视力)
  • 听觉障碍
  • 运动障碍
  • 认知障碍

法律要求

许多国家和地区有无障碍法律要求:

  • 美国:ADA、Section 508
  • 欧盟:EN 301 549
  • 中国:信息无障碍相关法规

SEO 收益

良好的无障碍实践同时能提升 SEO:

  • 语义化 HTML 有助于搜索引擎理解
  • 替代文本提供图片上下文
  • 清晰的导航改善用户体验

WCAG 原则

Web 内容无障碍指南(WCAG)遵循四个原则:

原则说明
可感知 (Perceivable)信息必须能被用户感知
可操作 (Operable)界面组件必须可操作
可理解 (Understandable)信息和操作必须可理解
健壮性 (Robust)内容必须兼容各种技术

语义化 HTML

正确使用标题层级

markdown
<!-- ✅ 正确:按层级使用 -->
# 一级标题

## 二级标题

### 三级标题

<!-- ❌ 错误:跳过层级 -->
# 一级标题

### 三级标题(跳过了二级)

使用语义化标签

vue
<template>
  <!-- ✅ 正确:使用语义化标签 -->
  <nav aria-label="主导航">
    <ul>
      <li><a href="/">首页</a></li>
      <li><a href="/guide">指南</a></li>
    </ul>
  </nav>

  <main>
    <article>
      <h1>文章标题</h1>
      <p>文章内容...</p>
    </article>
    
    <aside>
      <h2>相关链接</h2>
      <ul>...</ul>
    </aside>
  </main>

  <footer>
    <p>版权信息</p>
  </footer>
</template>

HTML5 语义标签

标签用途
<nav>导航区域
<main>主要内容
<article>独立内容块
<section>内容分组
<aside>侧边栏
<header>头部区域
<footer>底部区域
<figure>图片/图表容器

ARIA 标签

aria-label

为元素添加可访问标签:

vue
<template>
  <!-- 为图标按钮添加标签 -->
  <button aria-label="关闭菜单">
    <XIcon />
  </button>
  
  <!-- 为导航添加标签 -->
  <nav aria-label="面包屑导航">
    ...
  </nav>
</template>

aria-labelledby

关联现有元素作为标签:

vue
<template>
  <div>
    <h2 id="section-title">章节标题</h2>
    <section aria-labelledby="section-title">
      内容...
    </section>
  </div>
</template>

aria-describedby

提供更详细的描述:

vue
<template>
  <div>
    <input 
      type="text" 
      aria-describedby="password-hint"
    />
    <span id="password-hint">
      密码需包含至少8个字符
    </span>
  </div>
</template>

aria-hidden

隐藏装饰性元素:

vue
<template>
  <!-- 图标仅为装饰,对屏幕阅读器隐藏 -->
  <span aria-hidden="true">
    <StarIcon />
  </span>
</template>

role 属性

定义元素角色:

vue
<template>
  <!-- 自定义按钮 -->
  <div 
    role="button" 
    tabindex="0"
    @click="handleClick"
    @keydown.enter="handleClick"
  >
    点击我
  </div>
  
  <!-- 标签页 -->
  <div role="tablist">
    <button role="tab" aria-selected="true">标签1</button>
    <button role="tab" aria-selected="false">标签2</button>
  </div>
</template>

键盘导航

可聚焦元素

确保所有交互元素可通过键盘访问:

vue
<template>
  <!-- ✅ 原生可聚焦元素 -->
  <button>按钮</button>
  <a href="/">链接</a>
  <input type="text" />
  <select>...</select>
  
  <!-- ✅ 自定义元素添加 tabindex -->
  <div tabindex="0" @click="handleClick">
    自定义按钮
  </div>
</template>

tabindex 使用

tabindex 值说明
0可通过 Tab 键聚焦
-1仅可通过 JavaScript 聚焦
正数不推荐,会打乱焦点顺序

焦点管理

vue
<script setup>
import { ref, nextTick } from 'vue'

const modalVisible = ref(false)
const modalRef = ref(null)

async function openModal() {
  modalVisible.value = true
  await nextTick()
  // 将焦点移到模态框
  modalRef.value?.focus()
}

function closeModal() {
  modalVisible.value = false
  // 将焦点返回触发元素
  triggerButton.value?.focus()
}
</script>

<template>
  <div 
    v-if="modalVisible"
    ref="modalRef"
    role="dialog"
    aria-modal="true"
    tabindex="-1"
  >
    模态框内容
  </div>
</template>

焦点陷阱

在模态框中捕获焦点:

vue
<script setup>
import { onMounted, onUnmounted } from 'vue'

const props = defineProps({
  active: Boolean
})

function handleTabKey(e) {
  if (!props.active) return
  
  const focusableElements = modalRef.value.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  )
  
  const firstElement = focusableElements[0]
  const lastElement = focusableElements[focusableElements.length - 1]
  
  if (e.shiftKey && document.activeElement === firstElement) {
    lastElement.focus()
    e.preventDefault()
  } else if (!e.shiftKey && document.activeElement === lastElement) {
    firstElement.focus()
    e.preventDefault()
  }
}

onMounted(() => {
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Tab') handleTabKey(e)
  })
})
</script>

跳过链接

允许用户跳过重复内容:

vue
<template>
  <div>
    <!-- 跳过导航链接 -->
    <a href="#main-content" class="skip-link">
      跳转到主要内容
    </a>
    
    <nav>导航内容...</nav>
    
    <main id="main-content">
      主要内容...
    </main>
  </div>
</template>

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

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

图片无障碍

替代文本 (alt)

markdown
<!-- ✅ 有 alt 文本 -->
![VitePress Logo](/logo.svg "VitePress Logo")

<!-- ✅ 装饰性图片使用空 alt -->
![](/decorative-pattern.png)

<!-- ✅ 复杂图片使用 aria-describedby -->
![销售数据图表](/chart.png) aria-describedby="chart-desc"
<span id="chart-desc" class="sr-only">
  图表显示2023年销售额增长30%...
</span>

图片替代文本最佳实践

情况alt 文本
信息性图片描述图片内容
功能性图片描述功能(如"搜索")
装饰性图片空 alt=""
复杂图表简要描述 + 详细说明链接

颜色与对比度

对比度要求

WCAG 要求文本与背景的对比度:

文本类型最小对比度
普通文本4.5:1
大文本(18px+ 或 14px+ 粗体)3:1
非文本元素3:1

检查对比度

使用工具检查:

VitePress 配置

css
/* 确保足够对比度 */
:root {
  --vp-c-text-1: rgba(60, 60, 67);  /* 确保对比度足够 */
  --vp-c-text-2: rgba(60, 60, 67, 0.78);
}

/* 不要仅依赖颜色传达信息 */
.required::after {
  content: " *";
  color: #f00;
}

不仅依赖颜色

vue
<template>
  <!-- ❌ 错误:仅用颜色表示 -->
  <span class="error">错误</span>
  
  <!-- ✅ 正确:颜色 + 文字/图标 -->
  <span class="error">
    <ErrorIcon aria-hidden="true" />
    错误
  </span>
</template>

表单无障碍

标签关联

vue
<template>
  <!-- ✅ 正确:使用 label 包裹 -->
  <label>
    用户名
    <input type="text" name="username" />
  </label>
  
  <!-- ✅ 正确:使用 for 关联 -->
  <label for="email">邮箱</label>
  <input type="email" id="email" name="email" />
  
  <!-- ❌ 错误:没有标签 -->
  <input type="text" placeholder="请输入用户名" />
</template>

错误提示

vue
<template>
  <div>
    <label for="password">密码</label>
    <input 
      type="password" 
      id="password"
      aria-invalid="true"
      aria-describedby="password-error"
    />
    <span id="password-error" role="alert">
      密码长度不足8位
    </span>
  </div>
</template>

必填字段

vue
<template>
  <label>
    <span>用户名</span>
    <span aria-hidden="true" class="required">*</span>
    <span class="sr-only">(必填)</span>
    <input required aria-required="true" />
  </label>
</template>

表格无障碍

表格标题

markdown
<table>
  <caption>2023年销售数据</caption>
  <thead>
    <tr>
      <th scope="col">产品名称</th>
      <th scope="col">销售额</th>
      <th scope="col">增长率</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">产品A</th>
      <td>$100,000</td>
      <td>+15%</td>
    </tr>
  </tbody>
</table>

复杂表格

markdown
<table>
  <caption>课程安排表</caption>
  <thead>
    <tr>
      <th rowspan="2" scope="col">时间</th>
      <th colspan="2" scope="colgroup">课程类型</th>
    </tr>
    <tr>
      <th scope="col">理论课</th>
      <th scope="col">实验课</th>
    </tr>
  </thead>
  <tbody>
    ...
  </tbody>
</table>

媒体无障碍

视频

vue
<template>
  <video controls>
    <source src="video.mp4" type="video/mp4" />
    
    <!-- 字幕 -->
    <track 
      kind="subtitles" 
      src="subtitles-zh.vtt" 
      srclang="zh" 
      label="中文字幕" 
    />
    
    <!-- 音频描述 -->
    <track 
      kind="descriptions" 
      src="descriptions.vtt" 
      srclang="zh" 
      label="音频描述" 
    />
    
    您的浏览器不支持视频播放。
  </video>
</template>

音频

vue
<template>
  <audio controls>
    <source src="audio.mp3" type="audio/mpeg" />
    
    <!-- 文字稿链接 -->
    <a href="transcript.html">查看文字稿</a>
  </audio>
</template>

动态内容

实时区域

vue
<template>
  <!-- 状态更新 -->
  <div aria-live="polite">
    {{ statusMessage }}
  </div>
  
  <!-- 紧急通知 -->
  <div aria-live="assertive" role="alert">
    {{ errorMessage }}
  </div>
</template>

aria-live 类型

类型用途
polite非紧急更新,等用户空闲时播报
assertive紧急更新,立即播报
off不播报(默认)

加载状态

vue
<template>
  <div 
    role="status" 
    aria-live="polite" 
    aria-busy="isLoading"
  >
    <span v-if="isLoading">加载中...</span>
    <div v-else>内容</div>
  </div>
</template>

检查工具

自动化工具

  1. Lighthouse

    bash
    lighthouse https://yoursite.com --only-categories=accessibility
  2. axe DevTools

    • Chrome/Firefox 扩展
    • 检测 WCAG 违规
  3. WAVE

手动测试

  • [ ] 键盘导航测试(仅用 Tab 键)
  • [ ] 屏幕阅读器测试
  • [ ] 缩放测试(200% 放大)
  • [ ] 颜色对比度检查

屏幕阅读器

平台屏幕阅读器
WindowsNVDA(免费)、JAWS
macOSVoiceOver(内置)
iOSVoiceOver(内置)
AndroidTalkBack(内置)

VitePress 特定优化

配置改进

ts
// docs/.vitepress/config.mts
export default defineConfig({
  // 启用清晰的 URL
  cleanUrls: true,
  
  // 配置大纲级别
  outline: {
    level: [2, 3],
    label: '页面导航'
  },
  
  // 本地化文本
  returnToTopLabel: '返回顶部',
  sidebarMenuLabel: '菜单',
  darkModeSwitchLabel: '主题',
  
  themeConfig: {
    // 确保 logo 有替代文本
    logo: { 
      src: '/logo.svg',
      alt: 'VitePress Logo'
    }
  }
})

自定义组件无障碍

vue
<!-- 自定义容器组件 -->
<template>
  <div 
    :class="['custom-container', type]"
    role="region"
    :aria-label="title"
  >
    <div class="title">{{ title }}</div>
    <div class="content">
      <slot />
    </div>
  </div>
</template>

无障碍检查清单

发布前检查:

  • [ ] 所有图片有替代文本
  • [ ] 颜色对比度达标
  • [ ] 键盘可完全操作
  • [ ] 表单有正确标签
  • [ ] 标题层级正确
  • [ ] 链接文本有意义
  • [ ] 动态内容有通知
  • [ ] 视频有字幕
  • [ ] 焦点可见
  • [ ] 可缩放到 200%

相关资源

下一步

学习 插件扩展 了解如何扩展 VitePress 功能。

贡献者

加载中...

想要成为贡献者?

在 CNB 上参与贡献