PlantUML SVG accessibility: screen readers, WCAG, contrast

puml.online

PlantUML’s default SVG is hostile to visually impaired users — no alt text, screen readers can’t read it, contrast fails color-blind users. Compliance scenarios (government, education, finance) require WCAG 2.1 AA. This is how to make PlantUML diagrams accessible.

Why SVG accessibility matters

Three user groups:

  1. Visually impaired — use screen readers (NVDA / JAWS / VoiceOver)
  2. Motor impaired — use keyboard or switch devices
  3. Color blind — need color + shape/text dual encoding

PlantUML’s default SVG — no <title> <desc> role attributes on any element; screen readers hit “Image, no description” and skip.

WCAG 2.1 AA three hard metrics

Criterion Requirement PlantUML default
1.1.1 Non-text content Image must have alt or longdesc ✗ no alt
1.4.3 Contrast Text vs background contrast ≥ 4.5:1 ✓ default black on white usually passes
2.4.7 Focus visible Keyboard focus visible ✗ no tabindex
4.1.2 Name, role, value Controls have accessible name ✗ no aria-label

Fix 1: title and desc directives

PlantUML 1.2020+ supports inline <title> and <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 produces SVG with <title> element — screen reader announces “User Login Flow” as the diagram title.

But PlantUML’s title only emits <title>, not <desc> — for detailed description use post-processing.

Fix 2: caption directive (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 produces SVG with <desc> element — screen reader narrates the full description.

Fix 3: post-process to inject full a11y attributes

For a11y PlantUML doesn’t support natively, post-process with Python/Node:

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
# make_accessible.py
import sys, pathlib, re

svg = pathlib.Path(sys.argv[1]).read_text()
diagram_name = sys.argv[2] or "PlantUML Diagram"
diagram_desc = sys.argv[3] or "Architecture diagram"

# 1. inject role and aria-label into <svg> root
svg = re.sub(
r'<svg([^>]*)>',
f'<svg\\1 role="img" aria-label="{diagram_name}" aria-describedby="diagram-desc-{id(diagram_name)}">',
svg,
count=1,
)

# 2. inject <title> if PlantUML didn't
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)

# 3. inject <desc>
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)

# 4. add <title> child to each node <g> element
# PlantUML SVG nodes usually carry id="<alias>" or data-node-id
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,
)

# PlantUML nodes carry data-node-id attribute
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"

The SVG now has:

  • <svg role="img" aria-label="..." aria-describedby="...">
  • embedded <title> <desc>
  • every node <g tabindex="0" role="button" aria-label="...">

Screen reader can now narrate every node’s description, keyboard Tab key focuses each node.

Fix 4: contrast optimization

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 requires:

  • Text vs background contrast ≥ 4.5:1
  • Large text (≥18pt or bold 14pt) contrast ≥ 3:1

PlantUML default colors — black text on white #000000 vs #FFFFFF = 21:1 (perfect). But light gray on dark gray fails.

WCAG AAA strict requires contrast ≥ 7:1 — text must be pure or near-black.

Fix 5: color blind adaptation

8% of men / 0.5% of women are color blind — red-green is the most common. PlantUML’s default theme distinguishes by red/green:

1
2
3
4
skinparam sequence {
LifeLineBorderColor red
LifeLineBackgroundColor #FFEEEE
}

Red-green color blind can’t see this — need shape + text + color triple encoding:

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

Step numbers ①②③④ — color-blind users see order clearly too.

Fix 6: keyboard navigation post-process

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// keyboard-nav.js
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 focuses node → orange outline appears → Enter triggers click — fully keyboard-reachable.

Verification tools

WAVE (Web Accessibility Evaluation Tool)

Browser extension: https://wave.webaim.org/extension/

Open the page with the SVG → WAVE reports SVG a11y issues:

  • Missing alt text
  • Empty link
  • Missing form label

axe DevTools

DevTools extension: run axe → list all a11y violations.

Lighthouse

Chrome DevTools → Lighthouse → Accessibility score. Target 100/100.

Manual testing

  1. macOS VoiceOver: Cmd+F5 to enable → Tab through → should hear each SVG node’s description
  2. Windows NVDA: free screen reader → browse the page
  3. Keyboard only: unplug the mouse → Tab should focus every node

Field example: compliance-ready diagram

Government / medical / education / finance sites require WCAG 2.1 AA. PlantUML diagrams must:

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 "User Login Flow"
caption "Diagram showing user authentication with JWT token generation, 5 steps total. Involves Web App, Auth Service, and Database components."

skinparam defaultTextColor #000000
skinparam backgroundColor #FFFFFF
skinparam ArrowColor #000000
skinparam ParticipantPadding 20
skinparam BoxPadding 15

actor "User" as 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 : ③ Query user
DB --> Auth : ④ Return user record
Auth --> Web : ⑤ Return JWT
Web --> User : Redirect to home

@enduml

Supporting:

  • SVG post-process adds role="img" aria-label
  • Nodes get tabindex aria-label
  • Page has a text version of “diagram description” paragraph (<details><summary>Text Description</summary>...</details>)
  • Skip-navigation link lets users skip SVG

Key a11y HTML patterns

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">User Authentication Architecture</h3>
<p id="diagram-desc">Diagram showing the system authentication flow...</p>
</figcaption>
</figure>

<details>
<summary>📝 Text-version description (screen reader friendly)</summary>
<p>The user enters credentials in the browser. The web app forwards them to the auth service...</p>
</details>

<img alt=""> — empty alt tells screen reader to skip the image itself; figcaption provides the description.

Field foot-guns

  • PlantUML title doesn’t render longdesc — only emits <title>, detailed description needs caption or post-process-injected <desc>.
  • <svg role="img"> must pair with alt — WAVE flags “SVG missing alternative content”; add aria-label to <svg> itself.
  • Focus outline overridden by CSS — many themes set *:focus { outline: none }, keyboard users see no focus. Never blanket outline: none; only set on specific elements like buttons.
  • Color blind mode (Windows High Contrast) — SVG colors get force-replaced by OS, may become unreadable. Don’t rely on color alone.
  • Print styles — colored SVG prints black-and-white, color-blind friendly but loses color info. Use prefers-color-scheme media query to differentiate:
    1
    2
    3
    4
    @media print {
    svg .status-ok { fill: #000 !important; }
    svg .status-down { fill: #888 !important; }
    }

Decision tree

1
2
3
4
5
6
7
8
9
Need a11y?
├─ No (internal demo) → default SVG
├─ Public but non-compliant → post-process title/desc/role
├─ WCAG 2.1 AA → full post-process + focus + contrast
└─ WCAG 2.1 AAA (gov/medical) → strict contrast + text desc + testing

Is the diagram information-bearing?
├─ Yes → a11y required
└─ No (purely decorative) → alt="" to skip

Minimum a11y fix: add role="img" aria-label="..." to <svg>, post-process inject <title> <desc>screen reader can read the diagram — 30 lines of Python fixes 80% of a11y issues.

  • Title: PlantUML SVG accessibility: screen readers, WCAG, contrast
  • Author: puml.online
  • Created at : 2026-07-30 17:20:00
  • Updated at : 2026-08-14 21:34:29
  • Link: https://puml.online/blog/plantuml-svg-accessibility-en/
  • License: This work is licensed under CC BY-NC-SA 4.0.