演示文稿无障碍设计指南
原创
灵阙教研团队
B 基础 入门 |
约 12 分钟阅读
更新于 2026-02-28 AI 导读
演示文稿无障碍设计指南 对比度、替代文本与结构化标记的工程实现 1. 为什么演示文稿需要无障碍设计 无障碍设计不仅关乎合规(如 WCAG 2.1、Section 508、残疾人保障法),更关乎覆盖面。全球约 15% 的人口有某种形式的残障,包括视力障碍、色觉缺陷、听力障碍、运动障碍和认知障碍。一份无法被屏幕阅读器解析的 PPT,等于直接排斥了这部分受众。 无障碍的四个维度:...
演示文稿无障碍设计指南
对比度、替代文本与结构化标记的工程实现
1. 为什么演示文稿需要无障碍设计
无障碍设计不仅关乎合规(如 WCAG 2.1、Section 508、残疾人保障法),更关乎覆盖面。全球约 15% 的人口有某种形式的残障,包括视力障碍、色觉缺陷、听力障碍、运动障碍和认知障碍。一份无法被屏幕阅读器解析的 PPT,等于直接排斥了这部分受众。
无障碍的四个维度:
+--------------+ +--------------+
| 可感知 | | 可操作 |
| (Perceivable)| | (Operable) |
| 看得到/听得到| | 能用键盘操作 |
+--------------+ +--------------+
+--------------+ +--------------+
| 可理解 | | 健壮性 |
|(Understandable) | (Robust) |
| 内容易懂 | | 兼容辅助技术 |
+--------------+ +--------------+
1.1 演示文稿的特殊挑战
| 挑战 | 说明 | 影响人群 |
|---|---|---|
| 视觉依赖 | PPT 高度依赖视觉传达 | 视力障碍、色盲 |
| 时间限制 | 演示按固定节奏推进 | 认知障碍、阅读困难 |
| 多模态 | 文字+图片+图表+动画混合 | 多种障碍类型 |
| 格式多样 | PPTX/PDF/HTML 各有无障碍短板 | 辅助技术用户 |
| 线上/线下 | 现场投影 vs 远程共享 vs 异步阅读 | 听力障碍(远程) |
2. 对比度标准
2.1 WCAG 对比度要求
| 级别 | 标准文字 (>= 14pt) | 大文字 (>= 18pt 或 14pt 粗) |
|---|---|---|
| AA | >= 4.5:1 | >= 3:1 |
| AAA | >= 7:1 | >= 4.5:1 |
演示文稿建议至少达到 AA 级别,标题文字(通常 >= 18pt)需 3:1,正文需 4.5:1。
2.2 常见问题对比度
问题组合 (对比度不足):
浅灰文字 #9CA3AF 在白色背景 #FFFFFF 上 --> 2.6:1 (不通过)
白色文字 #FFFFFF 在浅蓝背景 #93C5FD 上 --> 2.2:1 (不通过)
黄色文字 #FCD34D 在白色背景 #FFFFFF 上 --> 1.6:1 (不通过)
安全组合 (对比度充足):
深灰文字 #374151 在白色背景 #FFFFFF 上 --> 10.9:1 (AAA)
白色文字 #FFFFFF 在深蓝背景 #1E40AF 上 --> 8.6:1 (AAA)
深灰文字 #1F2937 在浅灰背景 #F3F4F6 上 --> 14.7:1 (AAA)
2.3 对比度自动检查
function luminance(r, g, b) {
const [rs, gs, bs] = [r, g, b].map(c => {
c = c / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
function contrastRatio(color1, color2) {
const l1 = luminance(...hexToRgb(color1));
const l2 = luminance(...hexToRgb(color2));
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
function checkSlideContrast(slide) {
const issues = [];
for (const element of slide.elements) {
if (element.type === 'text') {
const textColor = element.color;
const bgColor = getBackgroundColorAt(slide, element.position);
const ratio = contrastRatio(textColor, bgColor);
const isLargeText = element.fontSize >= 18 ||
(element.fontSize >= 14 && element.bold);
const threshold = isLargeText ? 3.0 : 4.5;
if (ratio < threshold) {
issues.push({
element: element.id,
type: 'insufficient_contrast',
actual: ratio.toFixed(2),
required: threshold,
textColor,
bgColor,
fix: suggestBetterColor(textColor, bgColor, threshold)
});
}
}
}
return issues;
}
function suggestBetterColor(textColor, bgColor, targetRatio) {
// 逐步加深/加浅文字颜色直到满足对比度
let [r, g, b] = hexToRgb(textColor);
const bgLum = luminance(...hexToRgb(bgColor));
for (let i = 0; i < 100; i++) {
const textLum = luminance(r, g, b);
const ratio = (Math.max(textLum, bgLum) + 0.05) /
(Math.min(textLum, bgLum) + 0.05);
if (ratio >= targetRatio) {
return rgbToHex(r, g, b);
}
// 如果文字比背景亮,加深;否则加亮
if (textLum > bgLum) {
r = Math.max(0, r - 5);
g = Math.max(0, g - 5);
b = Math.max(0, b - 5);
} else {
r = Math.min(255, r + 5);
g = Math.min(255, g + 5);
b = Math.min(255, b + 5);
}
}
return '#000000'; // 回退到黑色
}
3. 替代文本(Alt Text)
3.1 替代文本原则
| 元素类型 | Alt Text 策略 | 示例 |
|---|---|---|
| 装饰图片 | alt="" (空字符串) | 背景纹理、装饰线条 |
| 信息图片 | 描述图片传达的信息 | "公司新办公楼外景,位于成都高新区" |
| 图表 | 描述数据结论 + 提供数据表 | "柱状图显示Q4营收同比增长45%" |
| 图标 | 描述图标含义 | "邮件图标" |
| Logo | 品牌名称 | "合规网 Logo" |
| 截图 | 描述截图内容 | "产品仪表盘截图,显示实时用户数据" |
3.2 图表替代文本生成
def generate_chart_alt_text(chart_data: ChartData) -> str:
"""为图表自动生成结构化替代文本"""
parts = []
# 1. 图表类型与标题
parts.append(f"{chart_data.chart_type}图: {chart_data.title}")
# 2. 核心结论(最重要的信息)
if chart_data.key_insight:
parts.append(chart_data.key_insight)
# 3. 数据摘要
if chart_data.chart_type in ('bar', 'column'):
max_item = max(chart_data.series, key=lambda x: x.value)
min_item = min(chart_data.series, key=lambda x: x.value)
parts.append(
f"最高: {max_item.label} ({max_item.value}),"
f"最低: {min_item.label} ({min_item.value})"
)
elif chart_data.chart_type == 'line':
first = chart_data.series[0]
last = chart_data.series[-1]
trend = "上升" if last.value > first.value else "下降"
parts.append(
f"趋势{trend},从 {first.value} 到 {last.value}"
)
elif chart_data.chart_type in ('pie', 'donut'):
top3 = sorted(chart_data.series, key=lambda x: x.value, reverse=True)[:3]
desc = ",".join(f"{item.label} {item.value}%" for item in top3)
parts.append(f"主要组成: {desc}")
# 4. 完整数据表(供屏幕阅读器用户深入了解)
parts.append("详细数据: " + ",".join(
f"{item.label}: {item.value}" for item in chart_data.series
))
return "。".join(parts) + "。"
3.3 PPTX 中设置 Alt Text
// PptxGenJS 设置替代文本
slide.addImage({
path: 'chart.png',
x: 1, y: 1, w: 8, h: 5,
altText: '柱状图显示2025年四季度营收趋势。Q1 2300万,Q2 2800万,Q3 3200万,Q4 4500万。全年增长率45%。'
});
// 装饰图片设置为 presentational
slide.addImage({
path: 'decorative_bg.png',
x: 0, y: 0, w: 13.33, h: 7.5,
altText: '' // 空 = 装饰性
});
4. 结构化标记
4.1 阅读顺序
屏幕阅读器按照元素的阅读顺序遍历内容。如果阅读顺序错误,用户接收到的信息就是混乱的。
视觉布局: 错误阅读顺序:
+---------+---------+ 1. 左栏标题
| 标题 | 标题 | 2. 右栏标题
+---------+---------+ 3. 左栏内容
| 左栏 | 右栏 | 4. 右栏内容
| 内容 | 内容 | (标题/内容交叉,逻辑混乱)
+---------+---------+
正确阅读顺序:
1. 页面标题
2. 左栏标题
3. 左栏内容
4. 右栏标题
5. 右栏内容
4.2 HTML 语义化标记
<article class="slide" role="region" aria-label="第 5 页,共 20 页">
<h2 class="slide-title">用户增长分析</h2>
<div class="slide-content" role="group" aria-label="双栏内容">
<section aria-label="增长数据">
<h3>关键指标</h3>
<dl class="metrics">
<div class="metric-item">
<dt>月活用户</dt>
<dd>120 万</dd>
</div>
<div class="metric-item">
<dt>日活用户</dt>
<dd>45 万</dd>
</div>
</dl>
</section>
<section aria-label="趋势图表">
<h3>增长趋势</h3>
<figure role="img" aria-label="折线图显示用户量从1月50万增长到12月120万">
<div id="chart-container"></div>
<figcaption class="sr-only">
月度用户增长数据:1月50万,2月55万,3月62万,
4月70万,5月78万,6月85万,7月110万(新功能上线),
8月105万,9月108万,10月112万,11月115万,12月120万。
</figcaption>
</figure>
</section>
</div>
<aside class="slide-notes" aria-label="演讲者备注">
<p>强调7月的用户激增与新功能上线的关联。</p>
</aside>
</article>
4.3 PPTX 结构标记
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.enum.text import PP_ALIGN
def create_accessible_slide(prs, content):
slide = prs.slides.add_slide(prs.slide_layouts[1])
# 设置标题(屏幕阅读器优先读取)
title = slide.shapes.title
title.text = content['title']
# 设置阅读顺序
# PPTX 中阅读顺序由 shapes 在 XML 中的排列决定
# 将最重要的元素放在最前面
# 为每个形状设置 alt text
for shape in slide.shapes:
if hasattr(shape, 'alt_text'):
if shape.shape_id in content.get('decorative', []):
shape.alt_text = '' # 装饰元素
else:
shape.alt_text = content.get('alt_texts', {}).get(
shape.shape_id, shape.text or ''
)
return slide
5. 色彩无障碍
5.1 色盲安全配色
约 8% 的男性和 0.5% 的女性有某种程度的色觉缺陷。最常见的是红绿色盲。
标准配色: 色盲安全替代:
红 #EF4444 绿 #22C55E 蓝 #2563EB 橙 #F59E0B
(红绿色盲无法区分) (明度差异大,可区分)
额外区分手段:
- 图案/纹理: 条纹 vs 点状 vs 实心
- 形状: 圆形 vs 方形 vs 三角形
- 标签: 直接标注数值
- 线型: 实线 vs 虚线 vs 点线
5.2 色盲模拟检查
// 色盲模拟矩阵(基于 Brettel 算法)
const colorBlindMatrices = {
protanopia: [ // 红色盲
[0.567, 0.433, 0.000],
[0.558, 0.442, 0.000],
[0.000, 0.242, 0.758]
],
deuteranopia: [ // 绿色盲
[0.625, 0.375, 0.000],
[0.700, 0.300, 0.000],
[0.000, 0.300, 0.700]
],
tritanopia: [ // 蓝色盲
[0.950, 0.050, 0.000],
[0.000, 0.433, 0.567],
[0.000, 0.475, 0.525]
]
};
function simulateColorBlind(hex, type) {
const [r, g, b] = hexToRgb(hex).map(v => v / 255);
const matrix = colorBlindMatrices[type];
const simR = matrix[0][0] * r + matrix[0][1] * g + matrix[0][2] * b;
const simG = matrix[1][0] * r + matrix[1][1] * g + matrix[1][2] * b;
const simB = matrix[2][0] * r + matrix[2][1] * g + matrix[2][2] * b;
return rgbToHex(
Math.round(simR * 255),
Math.round(simG * 255),
Math.round(simB * 255)
);
}
function checkColorBlindSafety(palette) {
const types = ['protanopia', 'deuteranopia', 'tritanopia'];
const issues = [];
for (const type of types) {
const simulated = palette.map(c => simulateColorBlind(c, type));
// 检查模拟后的颜色之间是否仍可区分
for (let i = 0; i < simulated.length; i++) {
for (let j = i + 1; j < simulated.length; j++) {
const diff = colorDifference(simulated[i], simulated[j]);
if (diff < 30) { // CIEDE2000 差异阈值
issues.push({
type,
color1: palette[i],
color2: palette[j],
difference: diff,
suggestion: '添加图案或标签以区分这两种颜色'
});
}
}
}
}
return issues;
}
6. 键盘导航
6.1 HTML 演示的键盘支持
| 按键 | 功能 | ARIA 要求 |
|---|---|---|
| Tab | 切换到下一个可聚焦元素 | 焦点可见 |
| Shift+Tab | 切换到上一个可聚焦元素 | 焦点可见 |
| 右箭头 / 空格 | 下一页 | aria-live 通知 |
| 左箭头 | 上一页 | aria-live 通知 |
| Home | 第一页 | -- |
| End | 最后一页 | -- |
| Escape | 退出全屏 | -- |
| F | 进入全屏 | -- |
// 焦点管理与 ARIA 通知
function goToSlide(index) {
const slide = slides[index];
slide.classList.add('active');
// ARIA 实时通知
const announcer = document.getElementById('aria-announcer');
announcer.textContent = `第 ${index + 1} 页,共 ${total} 页。${slide.getAttribute('aria-label') || ''}`;
// 焦点移到新页面的第一个可聚焦元素
const firstFocusable = slide.querySelector(
'a, button, input, [tabindex="0"]'
) || slide;
firstFocusable.focus();
}
// ARIA 实时区域
// <div id="aria-announcer" role="status" aria-live="polite" aria-atomic="true" class="sr-only"></div>
6.2 焦点样式
/* 焦点指示器:必须可见且高对比度 */
.slide *:focus-visible {
outline: 3px solid #2563EB;
outline-offset: 2px;
border-radius: 2px;
}
/* 不要这样做: */
/* *:focus { outline: none; } <-- 删除焦点指示器 = 无障碍灾难 */
7. 动画与运动
7.1 减少运动偏好
部分用户对动画敏感(前庭功能障碍),操作系统提供了"减少运动"设置:
/* 尊重系统偏好 */
@media (prefers-reduced-motion: reduce) {
.slide-transition {
transition: none;
}
.slide-element {
animation: none;
}
.chart-animation {
animation-duration: 0.01ms; /* 瞬间完成,保留最终状态 */
}
}
// JavaScript 中检测
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
if (prefersReducedMotion) {
// 禁用所有非必要动画
// 图表直接显示最终状态
chartOptions.animation = false;
}
7.2 安全动画规则
| 规则 | 说明 |
|---|---|
| 禁止频闪 | 每秒闪烁不超过 3 次 |
| 避免大面积运动 | 全屏旋转/缩放可能引发眩晕 |
| 提供暂停控制 | 自动播放的动画必须可暂停 |
| 淡入优于滑入 | 淡入/淡出对运动敏感用户更友好 |
| 持续时间合理 | 单个动画 200-500ms,不要过长 |
8. 文字可读性
8.1 最小字号
| 使用场景 | 最小字号 | 推荐字号 |
|---|---|---|
| 投影(大屏) | 18px | 24-48px |
| 屏幕共享 | 16px | 20-36px |
| 打印 | 12px | 14-24px |
| 手机阅读 | 14px | 16-20px |
8.2 行间距与字间距
.slide-body {
line-height: 1.5; /* 至少 1.5 倍 */
letter-spacing: 0.02em; /* 中文微调字间距 */
word-spacing: 0.05em; /* 英文词间距 */
max-width: 70ch; /* 每行最多 70 字符 */
}
/* 阅读障碍友好设置 */
.dyslexia-friendly {
font-family: 'OpenDyslexic', 'Source Han Sans CN', sans-serif;
line-height: 1.8;
letter-spacing: 0.05em;
word-spacing: 0.12em;
}
8.3 语言标记
<!-- 主语言声明 -->
<html lang="zh-CN">
<!-- 混合语言段落 -->
<p>
本季度我们使用了
<span lang="en">Machine Learning</span>
技术来优化推荐算法。
</p>
9. 无障碍检查清单
9.1 自动化检查
function accessibilityAudit(presentation) {
const issues = [];
for (const slide of presentation.slides) {
// 1. 对比度检查
issues.push(...checkSlideContrast(slide));
// 2. Alt Text 检查
for (const img of slide.images) {
if (img.altText === undefined || img.altText === null) {
issues.push({
slide: slide.index,
element: img.id,
type: 'missing_alt_text',
severity: 'error',
message: '图片缺少替代文本'
});
}
}
// 3. 阅读顺序检查
if (!slide.readingOrderDefined) {
issues.push({
slide: slide.index,
type: 'undefined_reading_order',
severity: 'warning',
message: '未定义阅读顺序'
});
}
// 4. 最小字号检查
for (const text of slide.textElements) {
if (text.fontSize < 14) {
issues.push({
slide: slide.index,
element: text.id,
type: 'small_font',
severity: 'warning',
message: `字号 ${text.fontSize}px 可能不够清晰`
});
}
}
// 5. 颜色依赖检查
issues.push(...checkColorDependency(slide));
// 6. 标题层级检查
issues.push(...checkHeadingHierarchy(slide));
}
return {
issues,
score: calculateA11yScore(issues),
summary: {
errors: issues.filter(i => i.severity === 'error').length,
warnings: issues.filter(i => i.severity === 'warning').length,
info: issues.filter(i => i.severity === 'info').length,
}
};
}
9.2 人工检查清单
| 检查项 | 验证方法 | 通过标准 |
|---|---|---|
| 对比度 | 自动工具检查 | 所有文字达到 AA |
| 替代文本 | 逐图检查 | 每个信息图都有描述 |
| 阅读顺序 | Tab 键遍历 | 逻辑顺序正确 |
| 键盘导航 | 纯键盘操作 | 所有功能可达 |
| 色盲安全 | 灰度模式查看 | 信息不依赖颜色 |
| 动画安全 | 开启减少运动 | 核心内容不受影响 |
| 字号可读 | 投影测试 | 最后排可读 |
| 语言标记 | 屏幕阅读器测试 | 正确朗读 |
10. 工具与资源
| 工具 | 用途 | 平台 |
|---|---|---|
| axe DevTools | 自动化无障碍检查 | 浏览器扩展 |
| WAVE | Web 无障碍评估 | 在线工具 |
| Color Oracle | 色盲模拟 | 桌面应用 |
| Contrast Checker | 对比度计算 | webaim.org |
| NVDA | 屏幕阅读器测试 | Windows |
| VoiceOver | 屏幕阅读器测试 | macOS/iOS |
| PowerPoint 辅助功能检查器 | PPTX 无障碍检查 | Office 内置 |
| PAC 3 | PDF 无障碍检查 | 桌面应用 |
Maurice | maurice_wen@proton.me