PlantUML SVG 无障碍:屏幕阅读器、WCAG、配色对比度
PlantUML 默认 SVG 对视障用户不友好——没有 alt 文本、屏幕阅读器读不出来、配色对色盲对比度不够。合规场景 (政府、教育、金融)要求 WCAG 2.1 AA。这篇是怎么让 PlantUML 图可访问。
为什么 SVG 要无障碍 3 个用户群体 :
视障 ——用屏幕阅读器(NVDA / JAWS / VoiceOver)
运动障碍 ——用键盘或开关设备导航
色盲 ——需要颜色 + 形状/文字双重编码
PlantUML 默认 SVG ——所有 element 没有 <title> <desc> role 属性,屏幕阅读器读到「Image, no description」就跳过 。
WCAG 2.1 AA 三条硬指标
准则
要求
PlantUML 默认
1.1.1 Non-text content
图必须有 alt 或 longdesc
✗ 无 alt
1.4.3 Contrast
文字背景对比度 ≥ 4.5:1
✓ 默认黑底白通常满足
2.4.7 Focus visible
键盘焦点可见
✗ 无 tabindex
4.1.2 Name, role, value
控件有可访问名
✗ 无 aria-label
修法 1:title 和 desc 指令 PlantUML 1.2020+ 支持内嵌 <title> 和 <desc>:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @startuml login_flow title "User Login Flow" actor User participant "Web App" as Web participant "Auth Service" as Auth database "Database" as DB User -> Web : Enter credentials Web -> Auth : POST /login Auth -> DB : SELECT user WHERE email = ? DB --> Auth : user record Auth --> Web : 200 OK + JWT Web --> User : Redirect to dashboard @enduml
title 生成的 SVG 自带 <title> 元素——屏幕阅读器读「User Login Flow」作为图标题 。
但 PlantUUM 的 title 只生成 <title>,不生成 <desc> ——更详细描述需要 post-process。
修法 2:caption 指令(1.2023+) 1 2 3 4 5 6 7 8 9 10 11 @startuml caption "Detailed flow showing user authentication with JWT token generation. Steps: 1) User enters credentials, 2) Web forwards to Auth Service, 3) Auth checks Database, 4) JWT returned." title "User Login Flow" actor User participant "Auth Service" as Auth User -> Auth : credentials Auth --> User : JWT @enduml
caption 生成的 SVG 有 <desc> 元素——屏幕阅读器朗读详细描述 。
修法 3:Post-process 注入完整 a11y 属性 PlantUML 不支持的内置 a11y,用 Python/Node post-process:
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 import sys, pathlib, resvg = pathlib.Path(sys.argv[1 ]).read_text() diagram_name = sys.argv[2 ] or "PlantUML Diagram" diagram_desc = sys.argv[3 ] or "Architecture diagram" svg = re.sub( r'<svg([^>]*)>' , f'<svg\\1 role="img" aria-label="{diagram_name} " aria-describedby="diagram-desc-{id (diagram_name)} ">' , svg, count=1 , ) if '<title>' not in svg: title_elem = f'<title>{diagram_name} </title>' svg = svg.replace('<svg' , f'<svg' , 1 ) svg = svg.replace('>' , f'>{title_elem} ' , 1 ) desc_elem = f'<desc id="diagram-desc-{id (diagram_name)} ">{diagram_desc} </desc>' if '<desc' not in svg: svg = svg.replace('</title>' , f'</title>{desc_elem} ' , 1 ) def add_title_to_node (match ): full = match .group(0 ) node_id = match .group(1 ) label = match .group(2 ) return full.replace( f'id="{node_id} "' , f'id="{node_id} " aria-label="{label} " tabindex="0" role="button"' , 1 , ) svg = re.sub( r'<g[^>]*data-node-id="(\w+)"[^>]*>(.*?)(?=</g>)' , lambda m: m.group(0 ).replace( f'data-node-id="{m.group(1 )} "' , f'data-node-id="{m.group(1 )} " tabindex="0" role="button" aria-label="Node {m.group(1 )} "' , 1 , ), svg, ) pathlib.Path(sys.argv[1 ]).write_text(svg)
1 python make_accessible.py architecture.svg "User Authentication Architecture" "Diagram showing user login flow with JWT token generation"
生成的 SVG 现在:
<svg role="img" aria-label="..." aria-describedby="...">
内嵌 <title> <desc>
每个节点 <g tabindex="0" role="button" aria-label="...">
屏幕阅读器 现在能朗读每个节点的描述,键盘 Tab 键可以聚焦每个节点。
修法 4:对比度优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @startuml skinparam defaultTextColor #000000 skinparam defaultFontSize 14 skinparam backgroundColor #FFFFFF skinparam ArrowColor #000000 skinparam ComponentBorderColor #000000 skinparam NoteBackgroundColor #FFFFCC skinparam NoteBorderColor #000000 skinparam NoteFontColor #000000 component "Auth Service" as auth component "User Service" as user user --> auth : HTTP @enduml
WCAG AA 要求:
文字 vs 背景对比度 ≥ 4.5:1
大字体(≥18pt 或粗 14pt)对比度 ≥ 3:1
PlantUML 默认配色 ——黑文字白底 #000000 vs #FFFFFF = 21:1(完美)。但浅灰背景 + 深灰文字 = 失败。
WCAG AAA 严格场景 对比度 ≥ 7:1——文字颜色必须纯黑或近黑。
修法 5:色盲适配 8% 男性 / 0.5% 女性是色盲 ——红绿色盲最常见。PlantUML 默认主题红绿区分:
1 2 3 4 skinparam sequence { LifeLineBorderColor red LifeLineBackgroundColor #FFEEEE }
红绿色盲看不到 ——需要形状 + 文字 + 颜色 三重编码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 skinparam Participant { BackgroundColor #E0E0E0 BorderColor #000000 FontColor #000000 FontStyle bold } skinparam Actor { BackgroundColor #FFE0B2 BorderColor #000000 } actor User <<Human>> participant "Auth" <<Service>> as auth database "DB" <<Storage>> as db User -> auth : ① login auth -> db : ② query db --> auth : ③ result auth --> User : ④ JWT @enduml
步骤编号 ①②③④ ——色盲也能看清顺序。
修法 6:键盘导航 post-process 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 document .querySelectorAll ('svg [data-node-id]' ).forEach (node => { node.setAttribute ('tabindex' , '0' ); node.setAttribute ('role' , 'button' ); node.addEventListener ('keydown' , (e ) => { if (e.key === 'Enter' || e.key === ' ' ) { e.preventDefault (); node.dispatchEvent (new MouseEvent ('click' )); } }); node.addEventListener ('focus' , () => { node.style .outline = '3px solid #FF6B00' ; }); node.addEventListener ('blur' , () => { node.style .outline = '' ; }); });
Tab 键聚焦节点 → 看到橙色轮廓 → Enter 键触发 click ——完整键盘可达。
验证工具 浏览器插件:https://wave.webaim.org/extension/
打开含 SVG 的页面 → WAVE 报告 SVG 的 a11y 问题:
Missing alt text
Empty link
Missing form label
DevTools 插件:运行 axe → 列出所有 a11y 违规。
Lighthouse Chrome DevTools → Lighthouse → Accessibility 评分。目标 100/100 。
手测
macOS VoiceOver :Cmd+F5 启用 → Tab 键浏览 → 应能听到 SVG 节点描述
Windows NVDA :免费屏幕阅读器 → 浏览页面
键盘 only :拔掉鼠标 → Tab 键应能聚焦所有节点
实战:合规场景的图 政府 / 医疗 / 教育 / 金融 网站要求 WCAG 2.1 AA。PlantUML 图要:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @startuml title "用户登录流程" caption "图示用户从输入凭证到获取 JWT token 的完整流程,共 5 个步骤。涉及 Web 应用、认证服务、数据库三个组件。" skinparam defaultTextColor #000000 skinparam backgroundColor #FFFFFF skinparam ArrowColor #000000 skinparam ParticipantPadding 20 skinparam BoxPadding 15 actor "用户" as User participant "Web 应用" as Web participant "认证服务" as Auth database "数据库" as DB User -> Web : ① 输入凭证 Web -> Auth : ② POST /login Auth -> DB : ③ 查询用户 DB --> Auth : ④ 返回用户记录 Auth --> Web : ⑤ 返回 JWT Web --> User : 重定向到首页 @enduml
配套 :
SVG post-process 加 role="img" aria-label
节点加 tabindex aria-label
页面有文字版「图描述」段落(<details><summary>文字描述</summary>...</details>)
跳过导航链接允许跳过 SVG
关键 a11y HTML 模式 1 2 3 4 5 6 7 8 9 10 11 12 <figure role ="figure" aria-labelledby ="diagram-title" aria-describedby ="diagram-desc" > <img src ="architecture.svg" alt ="" > <figcaption > <h3 id ="diagram-title" > 用户认证架构</h3 > <p id ="diagram-desc" > 图示系统认证流程...</p > </figcaption > </figure > <details > <summary > 📝 文字版描述(适合屏幕阅读器)</summary > <p > 用户从浏览器进入登录页面,Web 应用接收凭证后转发到认证服务...</p > </details >
<img alt="">——空 alt 让屏幕阅读器跳过图本身,figcaption 提供描述 。
实战踩坑
PlantUML title 不渲染 longdesc ——只生成 <title>,详细描述要么用 caption,要么 post-process 注入 <desc>。
<svg role="img"> 必须配合 alt ——WAVE 会报「SVG missing alternative content」,给 <svg> 自身加 aria-label 。
焦点轮廓被 CSS 覆盖 ——很多主题设 *:focus { outline: none },导致键盘用户看不到聚焦。禁止全局 outline: none,只在 button 之类显式设置 。
色盲模式(Windows 高对比度) ——SVG 颜色被 OS 强制替换,可能不可读。确保不依赖颜色单独区分 。
打印样式 ——彩色 SVG 打印成黑白,色盲友好但失去颜色信息。用 prefers-color-scheme 媒体查询区分 :1 2 3 4 @media print { svg .status-ok { fill : #000 !important ; } svg .status-down { fill : #888 !important ; } }
决策树 1 2 3 4 5 6 7 8 9 需要 a11y 吗? ├─ 不需要(内部 demo)→ 默认 SVG ├─ 公开页面但非合规 → post-process 加 title/desc/role ├─ WCAG 2.1 AA → 完整 post-process + 焦点 + 对比度 └─ WCAG 2.1 AAA(政府/医疗) → 严格对比度 + 文字版描述 + 测试 图有信息意义吗? ├─ 是 → 必须 a11y └─ 否(纯装饰) → alt="" 跳过
最小 a11y 修法 :给 <svg> 加 role="img" aria-label="...",post-process 注入 <title> <desc>,屏幕阅读器就能读图了 ——30 行 Python 解决 80% 的 a11y 问题。