How to Create Accessible SVG Exports with `role="img"` and `aria-labelledby` in Diagram Design

Diagram Design automatically generates WCAG 2.1 AA compliant SVG exports by enforcing a strict markup contract that assigns role="img" to every root <svg> element, links accessible names via aria-labelledby to prefixed <title> and <desc> IDs, and validates these rules through automated self-checking scripts.

The cathrynlavery/diagram-design repository produces diagrams that meet accessibility standards without manual intervention. Every exported SVG follows a programmatically enforced contract ensuring screen-reader compatibility through semantic markup and collision-free identifier management.

The Accessibility Contract

Diagram Design implements a four-part accessibility contract for every SVG export. This contract guarantees that assistive technologies can interpret diagrams as meaningful images rather than decorative graphics.

Semantic Role Assignment

Each SVG root element receives role="img" to signal to screen readers that the element represents a graphical image. This static attribute overrides the default SVG behavior, ensuring the element enters the accessibility tree as a single cohesive unit rather than exposing individual geometric paths to assistive technology.

Programmatic Label Association

The aria-labelledby attribute references two child elements by their id values: the <title> element providing the accessible name and the <desc> element providing the extended description. According to the source code in template.html, the attribute concatenates these IDs with a space separator:

<svg viewBox="0 0 1000 600"
     xmlns="http://www.w3.org/2000/svg"
     role="img"
     aria-labelledby="my-diagram-title my-diagram-desc">

Slug-Based ID Prefixing

To prevent collisions when multiple SVGs appear on the same page, Diagram Design prefixes every ID with the diagram's unique slug. The rendering engine appends -title and -desc suffixes to the base slug (e.g., my-diagram-title and my-diagram-desc), ensuring unique identifiers across inlined graphics.

Document Order Requirements

The self_check.py script enforces that <title> and <desc> must appear as the first two children of the SVG element (verified at lines 235-236). This strict ordering ensures screen readers encounter the accessible name before any graphical content.

Implementation in the Source Code

The accessibility features are hardcoded into the rendering pipeline and validated through automated tooling.

Template Injection

The core HTML scaffold in skills/diagram-design/assets/template.html injects the required attributes dynamically. When rendering occurs, the template replaces placeholder values with the actual diagram slug, generating the complete aria-labelledby value and corresponding ID attributes.

Automated Validation with self_check.py

The check_svgs function within skills/diagram-design/scripts/self_check.py (lines 214-244) parses generated SVGs and validates five specific rules:

  • The root <svg> element contains role="img"
  • The first child element is <title>
  • Both <title> and <desc> contain non-empty text content
  • The aria-labelledby attribute references the title ID followed by the desc ID
  • All referenced IDs match the expected slug-prefix pattern

CI Enforcement via lint-skin.py

The repository's linter (lint-skin.py) includes an a11y category that executes during continuous integration. This linter rejects any SVG failing the accessibility contract, preventing non-compliant diagrams from reaching production.

Practical Code Examples

Minimal Accessible SVG Export

The following markup represents the default output generated by Diagram Design when rendering a simple flowchart:

<svg viewBox="0 0 1000 600"
     xmlns="http://www.w3.org/2000/svg"
     role="img"
     aria-labelledby="example-title example-desc">
  <title id="example-title">Example Diagram</title>
  <desc id="example-desc">Shows a simple flow from A to B.</desc>
  <rect x="100" y="100" width="300" height="200" fill="#eee"/>
  <text x="250" y="210" text-anchor="middle">A → B</text>
</svg>

This structure matches the template found in example-flowchart.html within the assets folder.

Custom Slug Configuration

When generating multiple diagrams for a single page, control the identifier prefix by setting the diagram-slug parameter in your rendering configuration:

<svg viewBox="0 0 1000 600"
     xmlns="http://www.w3.org/2000/svg"
     role="img"
     aria-labelledby="user-journey-title user-journey-desc">
  <title id="user-journey-title">User Journey Map</title>
  <desc id="user-journey-desc">Steps a user takes from sign-up to purchase.</desc>
  <!-- generated geometry -->
</svg>

The slug user-journey generates unique IDs that will not conflict with other diagrams using different slugs.

Validating Your Exports

Run the bundled validation script against any generated HTML file to verify accessibility compliance:

python3 skills/diagram-design/scripts/self_check.py path/to/generated.html

Successful validation prints OK. If the SVG violates the contract, the script emits specific errors such as:


svg 1 needs role=img
svg 1 title must be its first child
svg 1 needs non-empty title and desc
svg 1 aria-labelledby must name title then desc

These messages originate from the validation logic in self_check.py and indicate exactly which accessibility requirement failed.

Summary

  • Diagram Design enforces accessible SVG exports by automatically injecting role="img" and aria-labelledby attributes into every generated diagram.
  • ID collision prevention occurs through automatic slug-prefixing of <title> and <desc> identifiers.
  • Validation happens at multiple layers: the self_check.py script checks individual files, while lint-skin.py prevents non-compliant merges in CI/CD pipelines.
  • Document order matters: the <title> element must be the first child of the SVG, followed immediately by <desc>, as enforced by lines 235-236 of the checking script.

Frequently Asked Questions

Why does Diagram Design use aria-labelledby instead of aria-label?

aria-labelledby creates a programmatic association between the SVG and its visible (or hidden) text elements, allowing the same content to serve both sighted users and assistive technology without duplication. This approach also enables more complex descriptions through the separate <desc> element, whereas aria-label supports only plain text strings and obscures the content from translation workflows.

How does the repository prevent ID collisions when multiple SVGs appear on one page?

The rendering engine prefixes every ID with the diagram's unique slug (e.g., my-diagram-title instead of generic title). This naming convention ensures that even when twenty diagrams are inlined within a single HTML document, each aria-labelledby reference points to the correct, unique elements without namespace conflicts.

What happens if the self-check script finds accessibility violations?

The script exits with an error code and prints specific diagnostic messages indicating which contract rules failed (such as missing role="img" or incorrect child element ordering). In the continuous integration environment, lint-skin.py catches these same violations and fails the build, preventing inaccessible diagrams from being merged into the main branch.

Where is the accessibility validation enforced in the CI/CD pipeline?

The a11y category within lint-skin.py executes during pull request checks, scanning all generated SVGs and template outputs. Additionally, the test suite in test-self-check.py provides automated regression testing, ensuring that future refactors to the rendering engine cannot unintentionally break the accessibility contract.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →