agentsclimarketplace

Svg

Skill mgifford/accessibility-skills/skills/svg

A collection of Claude Skill to mirror https://mgifford.github.io/ACCESSIBILITY.md

Install
npx -y skills add mgifford/accessibility-skills --skill svg

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its author says it does

Copied from the file, not written here

Load this skill whenever the project contains SVG graphics — inline SVGs, external SVG files, SVG icons, SVG illustrations, or SVG-based data visualizations. Under no circumstances use SVG without proper accessible titles, descriptions, and ARIA roles where required. Absolutely always add <title> and <desc> to meaningful SVGs and set role="img" with aria-labelledby pointing to those elements. Also apply this skill whenever the project accepts untrusted or user-uploaded SVG content, which requires sanitization before rendering.

SKILL.md

26.1 KB, as published. Nobody here has run it

SVG Accessibility Skill

Canonical source: examples/SVG_ACCESSIBILITY_BEST_PRACTICES.md in mgifford/ACCESSIBILITY.md This skill is derived from that file. When in doubt, the example is authoritative.

Apply these rules when creating, optimising, or reviewing SVG graphics. Only load this skill if the project contains SVGs.


Core Mandate

SVG accessibility depends on the graphic's purpose and how it is embedded. A decorative icon, an informative image, an icon inside a button, and an interactive diagram require different patterns. Start by deciding what users need from the graphic, then provide the simplest semantic implementation that delivers the same information or function. The presence of <title>, <desc>, role="img", or aria-label does not by itself make an SVG accessible — test the result in its final context.

SVG is also an active XML format that can contain scripts, event handlers, and external resource references. Untrusted SVG (uploads, pasted markup, AI-generated output) must be sanitized before rendering — see the Security section below.


Severity Scale (this skill)

LevelMeaning
CriticalSVG conveys essential information with no accessible alternative; untrusted SVG rendered without sanitization
SeriousSVG is interactive but unreachable or unlabelled for AT users
ModerateAT experience degraded but content still partially accessible
MinorBest-practice gap; marginal impact

Choose the Pattern by Purpose and Context

ContextPreferred pattern
Decorative SVG loaded with <img><img src="…" alt="">
Meaningful SVG loaded with <img><img src="…" alt="Useful description">
Decorative inline SVG<svg aria-hidden="true">…</svg>
Simple meaningful inline SVG<svg role="img" aria-labelledby="…"><title>…</title></svg>
Icon inside a named button or linkName the HTML control and hide the SVG
Complex graphicShort name plus visible summary, data, or detailed description
Reused sprite iconLabel or hide each outer <svg> instance, not the shared symbol
Interactive graphicPrefer native HTML controls; provide an equivalent structured alternative

Use native HTML semantics before ARIA. Do not add role="img" to an <img> merely because its source is an SVG file — that's redundant.


Critical: Decorative SVGs Must Be Hidden Completely from AT

<svg aria-hidden="true" focusable="false">
  <!-- decorative icon — no title, no desc, no role needed -->
</svg>

aria-hidden="true" removes the SVG and its descendants from the accessibility tree — do not place focusable or interactive content inside an element hidden this way. focusable="false" addresses legacy IE/older Edge behavior; it can remain for projects still supporting those browsers but isn't a universal modern requirement. Do not add <title>/<desc> to an intentionally-hidden SVG. Never use role="presentation" as a substitute for aria-hidden="true" — it does not reliably suppress announcement across browser/screen-reader combinations.

When is an SVG decorative? If the same meaning is conveyed by adjacent visible text, or the image is purely ornamental, it's decorative. If removing it would leave a user without needed information, it's meaningful.


External SVG Used with <img>

Follows the same text-alternative rules as other image formats.

<!-- Informative -->
<img src="/assets/images/service-area.svg" alt="Map showing the service area extending from Kingston to Ottawa" width="640" height="360">

<!-- Decorative -->
<img src="/assets/images/flourish.svg" alt="" width="120" height="24">

<!-- Functional (destination-describing) -->
<a href="/reports/annual-report.pdf">
  <img src="/assets/icons/download.svg" alt="Download annual report">
</a>

role="img" is redundant on <img>; aria-label unnecessary when alt can provide the name. Internal SVG <title>/<desc> does not replace HTML alt when loaded through <img> — the HTML alt is what's read.


Serious: Simple Meaningful Inline SVG

<svg role="img" aria-labelledby="download-icon-title" viewBox="0 0 24 24">
  <title id="download-icon-title">Download</title>
  <path d="M12 3v12m0 0 5-5m-5 5-5-5M5 21h14"></path>
</svg>

The <title> must be a direct child of <svg> with a unique ID, referenced via aria-labelledby. A plain aria-label is a reasonable alternative when managing a unique title ID adds needless complexity:

<svg role="img" aria-label="Download" viewBox="0 0 24 24">…</svg>

Do not use both patterns without a specific tested reason — ARIA labelling attributes take priority over <title> in the accessible-name calculation (aria-labelledby > aria-label > direct child <title>). Note: some browsers show <title> as a pointer tooltip — that is not a substitute for visible text or a real accessible tooltip; it may not be discoverable by keyboard, touch, or speech-input users.


Serious: Icons Inside Buttons and Links

The HTML control should own the accessible name — hide the SVG icon.

<!-- Visible text -->
<button type="button">
  <svg aria-hidden="true" viewBox="0 0 24 24"><path d="M6 6 18 18M18 6 6 18"></path></svg>
  Close
</button>

<!-- Icon-only -->
<button type="button" aria-label="Close dialog">
  <svg aria-hidden="true" viewBox="0 0 24 24"><path d="M6 6 18 18M18 6 6 18"></path></svg>
</button>

Do not give both the button and its hidden icon separate versions of the same name — that produces duplicate or inconsistent announcements. Prefer a visible label when the action may not be obvious — a familiar-looking icon can mean different things across products.

For an external SVG asset loaded via <img> inside an already-labelled button (e.g., a theme selector), use alt="" since the button supplies the name — note that an external <img>-loaded SVG does not inherit currentColor from the surrounding button; validate the asset's own colors on every background it appears on, or inline the paths if it must follow a manual theme.


Serious: Accessible Names and Descriptions — Correct ARIA Relationship

SVG accessible-name priority: aria-labelledby > aria-label > direct child <title> > other fallbacks. Descriptions: aria-describedby > direct child <desc>.

<svg role="img" aria-labelledby="weather-title" aria-describedby="weather-desc" viewBox="0 0 320 180">
  <title id="weather-title">Weekly temperature trend</title>
  <desc id="weather-desc">Temperatures rise from 12 degrees Monday to 21 degrees Friday.</desc>
</svg>

Keep IDs unique in the whole HTML document — a repeated component with fixed SVG IDs can cause one instance to be labelled by another. Do not put both title and description IDs in aria-labelledby — that concatenates both strings into the accessible name; use aria-labelledby for the name and aria-describedby for the description separately. Do not bury essential information in <desc> — support for navigating long SVG descriptions varies across AT; put essential details in visible HTML instead.


Moderate: Complex Graphics, Diagrams, and Charts

A complex SVG needs more than one long accessible name: a concise name, a visible summary of the purpose/conclusion, complete data in structured HTML when needed, and keyboard-operable controls if interactive.

<figure>
  <svg role="img" aria-labelledby="visitors-title" aria-describedby="visitors-summary" viewBox="0 0 640 360">
    <title id="visitors-title">Website visitors from January to March</title>
  </svg>
  <figcaption id="visitors-summary">
    Visitors increased each month, from 12,400 in January to 19,200 in March.
  </figcaption>
  <details>
    <summary>View the chart data</summary>
    <table>
      <caption>Monthly website visitors</caption>
      <thead><tr><th scope="col">Month</th><th scope="col">Visitors</th></tr></thead>
      <tbody><tr><th scope="row">January</th><td>12,400</td></tr></tbody>
    </table>
  </details>
</figure>

Do not duplicate a detailed table into aria-label/<title>/<desc> — long image-name strings are hard to navigate. The ARIA Graphics Module defines structured roles (graphics-document, graphics-object, graphics-symbol) but support must be tested with actual AT combinations; these don't remove the need for an equivalent text presentation. See skills/charts-graphs/SKILL.md for chart-specific requirements.


Reused Icons and SVG Sprites

<svg aria-hidden="true" hidden>
  <symbol id="icon-download" viewBox="0 0 24 24">
    <path d="M12 3v12m0 0 5-5m-5 5-5-5M5 21h14"></path>
  </symbol>
</svg>
<button type="button">
  <svg aria-hidden="true" viewBox="0 0 24 24"><use href="#icon-download"></use></svg>
  Download report
</button>

Names inside a shared symbol are rarely appropriate for every instance — "Arrow" could mean Next, Previous, Expand, or Collapse depending on context. Name the control or the outer rendered instance, not the shared symbol. An external <use> target must reference a real distinct element (usually a <symbol> with an ID) — do not create recursive self-references.


Moderate: Colour, Contrast, and Themes

.icon {
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-width: 2;
}

currentColor is useful, not mandatory — multi-colour graphics may need several semantic tokens; validate every meaningful pairing in every theme. Do not rely on colour alone — add text labels, patterns, line styles, or shapes for statuses/series/selected states. Text inside SVG is subject to text contrast requirements; required graphical information generally needs 3:1; decorative details don't need 3:1 merely for appearing in a meaningful graphic.

@media (forced-colors: active) {
  .icon { color: ButtonText; }
  .chart-line { fill: none; stroke: CanvasText; }
  .chart-line[data-selected="true"] { stroke: Highlight; stroke-width: 4; }
}

Do not set both fill and stroke on every path indiscriminately — it can turn line artwork into solid shapes. Avoid forced-color-adjust: none unless essential and tested against multiple system palettes.


Text Inside SVG

Keep essential labels visible and legible; meet text contrast requirements. Do not convert essential labels to paths merely to preserve a typeface, and do not use font-size="0", off-canvas positioning, or transparent SVG text as a substitute for a real text alternative. Provide important explanations in HTML. Test resizing/zoom — SVG labels can overlap or clip even when the outer SVG scales. <foreignObject> can embed HTML in SVG but adds rendering/fallback complexity — use ordinary HTML outside the SVG when it can provide the same result.


Critical: Interactive SVG

Prefer native HTML buttons, links, and disclosures placed near the graphic over interaction inside the SVG itself:

<div class="map">
  <svg aria-hidden="true" viewBox="0 0 640 360"><!-- Visual map --></svg>
  <ul class="map-locations">
    <li><a href="/locations/ottawa">Ottawa service centre</a></li>
  </ul>
</div>

If interaction must occur within the SVG: use SVG <a href="…"> for genuine navigation; provide a purpose-describing accessible name; preserve logical DOM/focus order; support expected keyboard activation; show a visible, unclipped focus indicator; meet target-size requirements; expose selected/ expanded/pressed states; ensure the accessible name contains the visible label for speech input; provide an equivalent HTML list/table when the visual layout can't be navigated non-visually. Do not put every data point in the tab order — hundreds of focus stops make a visualization technically reachable but practically unusable. Do not create a "button" by adding only tabindex="0" and a click listener to <path>/<g> — that provides no native button semantics or keyboard behaviour. An interactive SVG unreachable by keyboard is Critical.


Serious: Motion, Animation, and Flashing

.loading-icon { transform-origin: center; }
@media (prefers-reduced-motion: no-preference) {
  .loading-icon { animation: spin 1s linear infinite; }
}

Do not use a blanket svg * rule to reduce motion — it can affect unrelated components and override meaningful states. Target the specific animated component and define an understandable static state:

@media (prefers-reduced-motion: reduce) {
  .diagram-panel { animation: none; transition: none; }
}

For <animate>/<animateTransform>, use begin="indefinite" and only trigger via beginElement() when prefers-reduced-motion: no-preference is confirmed.

Rapid SVG animation is Critical if it flashes at 3+ Hz (WCAG 2.3.1) — this is not replaced by respecting reduced-motion preference; the flash threshold applies regardless. WCAG 2.2.2 requires pause/stop/hide controls for moving/blinking/auto-updating content lasting 5+ seconds and not essential. WCAG 2.3.3 (Animation from Interactions, AAA) requires users be able to disable non-essential interaction-triggered motion — respecting prefers-reduced-motion is an effective approach even at AA.


Responsive SVG and Zoom

Preserve viewBox so the graphic scales without cropping its internal coordinate system — its removal during optimization is not just Minor, it will break fluid layouts.

.responsive-diagram { display: block; max-inline-size: 100%; block-size: auto; }

Test at 200%/400% zoom and narrow viewports — a scalable canvas doesn't guarantee labels reflow or stay readable. For dense diagrams, consider a simplified small-screen view plus an adjacent HTML explanation. Never disable page zoom to compensate for an inflexible SVG layout.


Standalone SVG Documents

An SVG opened directly in a browser is a document, not an <img> — include namespace, language, and an accessible name:

<svg xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="diagram-title"
     viewBox="0 0 800 450" lang="en" xml:lang="en">
  <title id="diagram-title">Request approval workflow</title>
</svg>

If the standalone file is complex, link to an equivalent HTML description near where users open it.


Critical: Security, Sanitization, and Build Processing

SVG is an active XML format that can contain scripts, event handlers, links, external resources, CSS, animation, filters, and embedded HTML via <foreignObject>. A file that looks like an image must not automatically be treated as inert content. Accessibility metadata must survive processing, but never at the cost of weakening the security boundary — apply security and accessibility checks to the same final artifact.

Classify the SVG's trust level before processing:

SourceRequired treatment
Author-controlled (reviewed icons in the repo)Review changes, restrict build tooling, validate final output
Generated from a controlled source (Mermaid/chart renderer)Treat renderer + config as dependencies; validate generated SVG
Platform-generated (hosted diagram service)Record platform/renderer version; verify hosted result
Untrusted (uploads, pasted markup, user-edited, remote files, AI-generated)Enforce input limits, parse securely, sanitize with an SVG-aware allowlist, validate before display or export

Inline SVG, <object>, <embed>, and standalone documents have broader capabilities than <img> and need greater scrutiny — an SVG safe for <img> today may be downloaded, opened directly, or embedded inline by someone else later. There is no single accessibility treatment across all embedding contexts.

Sanitize untrusted SVG with a maintained, SVG-aware sanitizer/allowlist (e.g., DOMPurify with a reviewed SVG profile) — a generic HTML allowlist may remove essential SVG features or permit unneeded ones. Do not:

  • insert untrusted SVG using innerHTML before sanitization
  • treat well-formed XML as safe SVG
  • treat DOMParser, XMLSerializer, a formatter, or SVGO as a sanitizer
  • use regular expressions as an SVG sanitizer
  • assume a filename, MIME type, or successful image preview proves safety
  • modify sanitized markup with untrusted strings afterward

Sanitization should remove/reject: <script> and event-handler attributes (onclick); javascript: and other unapproved URL schemes; unexpected external href/xlink:href; external resources in CSS/@import/url(); <foreignObject>/embedded HTML unless explicitly required; unapproved animation/interactive elements; processing instructions, DTDs, external entities. Same-document #fragment references (<use href="#icon">, marker-end="url(#arrowhead)") are commonly needed and safe to allow — but validate every permitted fragment resolves to an allowed element in the same sanitized document; allowing fragments does not imply allowing arbitrary external URLs.

Parse XML securely when processed server-side: disable DTD processing and external entity resolution; prevent network/local-file access from the parser; avoid XSLT/XInclude unless explicitly required and secured; set input/nesting/entity limits; fail closed on parse/sanitization failure.

Limit resource consumption — SVG can exhaust CPU/memory without executing a script: cap input/decompressed size, element/attribute counts, path complexity, <use> nesting depth, filter complexity, embedded data-URL size, and rendered dimensions. Reject recursive references and content exceeding policy.

Preserve accessibility during optimization — configure the build to keep viewBox; <title>/<desc> when used; role/aria-*/language attributes; IDs referenced by ARIA, <use>, clip-paths, masks, filters, gradients; semantic <text>; focus/theme/forced-colors/reduced-motion styles; public sprite symbol IDs. Generate unique IDs for repeated inline components — an optimizer that collapses every copy to the same ID breaks both rendering and accessible names. Lock the optimizer version and test configuration changes against representative fixtures; use separate edit-safe vs. production optimization profiles.

Recommended processing sequence: (1) apply input/resource limits, (2) parse with secure XML settings, (3) sanitize untrusted input with the approved profile, (4) apply only controlled transformations, (5) optimize with a reviewed versioned config, (6) validate the final SVG for both security and accessibility, (7) insert/serve/export only the validated result. If any later step can introduce untrusted markup, sanitize again after that step.

A restrictive CSP reduces the effect of some unsafe SVG features but does not replace parsing, sanitization, resource limits, or output validation — use it as defence in depth, not a substitute.


What Is NOT Required

  • A <desc> for every SVG — <title> + aria-labelledby suffices when a name alone fully conveys meaning
  • Focus styling inside non-interactive SVGs
  • A forced-colors block when the SVG remains perceivable without it
  • role="img" on decorative SVGs — use aria-hidden="true" instead

Testing

  • Code review: classify each SVG as decorative/informative/functional/ complex/interactive; confirm embedding pattern matches; inspect name/ description relationships; search repeated inline components for duplicate IDs; confirm build optimization preserves required attributes; classify the source's trust level and identify all embedding contexts it can be used in; review sanitizer/parser/optimizer/renderer config changes; confirm final output validation covers both security and accessibility
  • Browser/AT: inspect the accessibility tree for expected role/name/ description; confirm decorative icons are silent and named graphics announced once; navigate all SVG controls by keyboard only; test speech- input names against visible labels; 200%/400% zoom and narrow viewports; every supported theme; prefers-reduced-motion/prefers-contrast/forced colors; colour-independent meaning; test with images unavailable when the SVG is essential
  • Security/processing: maintain valid AND adversarial fixtures; test the complete pipeline, not just the sanitizer in isolation — script/event handlers rejected; unsafe URL schemes rejected; unexpected external resources blocked; DTDs/external entities rejected without resolving; unapproved <foreignObject> rejected; recursive/excessive <use> rejected; oversized/complex input rejected; permitted same-document references still render; repeated inline instances keep unique IDs; optimized output still passes security rules and matches the accessible alternative. Run these whenever the sanitizer/parser/optimizer/renderer/allowlist/dependency version changes.

Automated tools can detect missing <img alt>, some accessible-name problems, duplicate IDs, contrast failures, and certain ARIA errors — cannot decide whether an alternative is equivalent, whether a graphic is truly decorative, or whether an interactive visualization is usable. No automated pass replaces manual SVG review.


Common Failures

FailureCorrection
<img src="graphic.svg"> has no altAdd context-appropriate alt, including alt="" for decoration
role="img" added to every HTML <img>Use native <img> semantics and alt
Both title and description IDs put in aria-labelledbyUse aria-labelledby for name, aria-describedby for description
A long table placed in <desc>Provide essential detail as visible, structured HTML
Repeated inline icons contain identical title IDsGenerate a unique ID per instance, or use direct aria-label
A sprite symbol references itselfPut real paths in the symbol; reference only from rendered instances
Forced-colours CSS sets both fill and stroke on every pathApply targeted system colours without changing geometry
A clickable <path> has only a click handlerUse a native HTML control or implement complete keyboard semantics
Reduced motion implemented with svg * and !importantTarget each animated component; define a meaningful static state
Well-formed XML treated as sanitized SVGParse securely and apply an SVG-aware allowlist sanitizer
A regex removes <script> and is called a sanitizerUse a maintained SVG-aware sanitizer with a documented policy
Sanitized SVG later modified with untrusted stringsKeep later transforms controlled, or sanitize again before final validation
CSP treated as a replacement for sanitizationUse CSP only as an additional layer
An optimizer update accepted because the SVG "still looks correct"Lock and test the optimizer; verify semantics, references, and alternatives

Definition of Done Checklist

  • Every SVG classified by purpose (decorative/informative/functional/complex/interactive) and embedding context
  • Decorative SVGs: aria-hidden="true" focusable="false", no <title>, no <desc>
  • Meaningful <img> SVGs have useful alt; decorative external images use alt=""
  • Meaningful inline SVGs: role="img" + <title> + aria-labelledby (or aria-label)
  • Complex SVGs: name + visible summary + structured HTML detail
  • Icons inside named controls don't duplicate the control's accessible name
  • Names/descriptions use the correct ARIA relationship (labelledby ≠ describedby)
  • All referenced IDs preserved through optimisation; unique across repeated instances
  • currentColor used where appropriate; non-text elements meet 3:1 contrast
  • viewBox present and preserved
  • Animated SVGs run only under prefers-reduced-motion: no-preference; no flashing at 3+ Hz regardless of preference
  • Interactive SVGs keyboard operable with visible focus; prefer native wrapper elements
  • Untrusted SVG sanitized with an SVG-aware allowlist before any innerHTML insertion
  • Scripts, event handlers, unsafe URL schemes, external resources, DTDs/entities rejected
  • Resource limits applied (size, element count, <use> depth, filter complexity)
  • Security and accessibility validation run after the last material transformation
  • Tested with screen reader on a supported OS/browser combination

Key WCAG Criteria

  • 1.1.1 Non-text Content (A) — Critical if meaningful SVG has no text alternative
  • 1.3.1 Info and Relationships (A)
  • 1.4.1 Use of Color (A)
  • 1.4.3 Contrast Minimum (AA)
  • 1.4.5 Images of Text (AA)
  • 1.4.11 Non-text Contrast (AA)
  • 2.1.1 Keyboard (A)
  • 2.2.2 Pause, Stop, Hide (A)
  • 2.3.1 Three Flashes or Below Threshold (A) — Critical for flashing/strobing animations
  • 2.3.3 Animation from Interactions (AAA)
  • 2.5.3 Label in Name (A)
  • 2.5.8 Target Size Minimum (AA)
  • 4.1.2 Name, Role, Value (A)

References

Standards horizon: The 3:1 non-text contrast ratio may change under WCAG 3.0's APCA model. The current 3:1 requirement remains applicable. Monitor: https://www.w3.org/TR/wcag-3.0/

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.