SVG无障碍
inline SVG 可以进无障碍树:每个图形节点都能有名称、角色、焦点。一旦把同一张图做成 <img src="*.svg"> 或背景图,树就没了,只剩一张图。可交互图(热区、流程图、白板)必须 inline,不要 img。
1. title / desc 与名称
<svg role="img" aria-labelledby="title-1 desc-1">
<title id="title-1">2026 Q3 销售额</title>
<desc id="desc-1">华东最高,东北最低。</desc>
<!-- 几何 -->
</svg>
<title> 第一子节点是原生 tooltip 和可访问名的常见来源,但三引擎对「自动当 accessible name」并不一致。生产显式 aria-labelledby 指向 title(和需要时的 desc),不要赌悬停提示等于读屏。
装饰性图标:宿主按钮已经有 accessible name 时,内联 SVG aria-hidden="true",避免双读。
export function labelGraphic(
el: SVGGraphicsElement,
name: string,
detail?: string,
): void {
const title = document.createElementNS("http://www.w3.org/2000/svg", "title");
title.textContent = name;
el.prepend(title);
el.setAttribute("aria-label", name);
if (detail) {
el.setAttribute("aria-description", detail);
}
}
复杂图还要同等文本:旁边 HTML 表或 aria-describedby。只靠一句 label 不够。
2. 角色:静态 vs 可交互
| 形态 | 角色 | 不要 |
|---|---|---|
| 静态图表/插画 | 根 role="img" + 名称 | 再给每个叶子 img |
| 一组可点热区 | 热区 role="button" 或真实 HTML overlay | 根再 role="img"(子树被压成一张图) |
| 应用画布 | 谨慎 role="application",必须自备全套键盘 | 默认给所有编辑器扣 application |
role="img" 会把后代从可访问树上折叠。可点的城市、可点的柱子,不要包在 img 里。
不要:<img src="map.svg" usemap> 当现代方案。要热区就 inline,或 HTML overlay 按钮用 getScreenCTM 对齐。
3. 键盘
可聚焦:tabindex="0"(或按顺序的正整数,尽量少用)。SVG 元素默认往往 不进 Tab 序。焦点可见:CSS :focus / :focus-visible 画描边,vector-effect: non-scaling-stroke 避免缩小时环消失。
export function enableGraphicFocus(el: SVGGraphicsElement): void {
el.setAttribute("tabindex", "0");
el.setAttribute("role", "button");
el.addEventListener("keydown", (event: KeyboardEvent) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
el.dispatchEvent(new Event("activate"));
}
});
}
方向键遍历节点、Delete 删除、Esc 清选:和指针命中改 同一份 selectedId。焦点在 A、鼠标选中 B 是缺陷。列表型侧栏可以当键盘主入口,svg tabindex="-1" 只鼠标,减少 application 角色。
触控热区在屏幕空间 ≥ 44 CSS 像素;细线用透明扩大命中,见 指针事件。
prefers-reduced-motion:关掉相机动画和闪烁选区。
4. 适用 / 不适用
适用:任何可点、可选、承载信息的 inline SVG。
不适用:纯装饰(aria-hidden);已是按钮内容的图标(跟按钮走);把编辑器假扮成一张 img 交差。
WCAG 对非文本内容、键盘、焦点可见、名称角色值的要求,SVG 不会豁免。<canvas> 必须平行 DOM;SVG 没借口却做成 img,是主动扔掉已有的树。
5. 失败形态
| 症状 | 原因 |
|---|---|
| 读屏只说「图像」 | 可交互图画成了 <img>,或根 role="img" 吞掉热区 |
| 有 title 但读屏无名称 | 没 aria-labelledby / aria-label,引擎没把 title 当名字 |
| Tab 进不去 | 没 tabindex |
| 焦点环缩没了 | 描边在用户空间,没 non-scaling-stroke |
| 键盘选中和鼠标不是同一个 | 两份 focus state |
| 按钮读两遍 | 图标没 aria-hidden,title 又念一遍 |
权威资料
核对日期:2026-08-26