| name | svg |
| description | 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.
|
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
Outcome-based, not checklist-driven. Measure against real usage context: is the
SVG perceivable, operable, and compatible with assistive technologies?
Pattern recommendations in this skill are grounded in cross-browser/screen reader
testing by Carie Fisher (published on Smashing Magazine, updated at
https://cariefisher.com/a11y-svg-updated/). When a pattern is labelled
"Best in Show", "Use Caution", or "Not Recommended" below, those
labels reflect her testing results across OS/browser/screen reader combinations.
Severity Scale (this skill)
| Level | Meaning |
|---|
| Critical | SVG conveys essential information with no accessible alternative |
| Serious | SVG is interactive but unreachable or unlabelled for AT users |
| Moderate | AT experience degraded but content still partially accessible |
| Minor | Best-practice gap; marginal impact |
Decorative SVGs — Hide Completely from AT
A decorative SVG that is not hidden from AT is a Moderate issue — it adds
noise to the accessibility tree and forces screen reader users to listen to
meaningless announcements.
Both attributes are required:
<svg aria-hidden="true" focusable="false">
</svg>
aria-hidden="true" removes the SVG from the accessibility tree entirely
focusable="false" is required for IE/Edge legacy compatibility where SVGs
become focusable by default and appear in the tab order
Never use role="presentation" as a substitute for aria-hidden="true"
on decorative SVGs. role="presentation" does not reliably suppress AT
announcement across all browser/screen reader combinations.
When is an SVG decorative? If the same meaning is conveyed by adjacent
visible text, or if the image is purely ornamental (borders, backgrounds,
flourishes), it is decorative. If removing it would leave a user without
information or context they need, it is meaningful.
Meaningful SVGs — Choosing the Right Pattern
Based on Carie Fisher's testing across NVDA, JAWS, VoiceOver (macOS and iOS),
TalkBack, and Narrator:
SVG as <img> (external file reference)
Use <img> for simple, uncomplicated images. The SVG file loads separately —
this is lighter and easier to maintain for icons used in multiple places, but
offers less control over internals.
Best in Show — <img> patterns:
<img role="img" class="icon" alt="Download" src="download.svg">
<img role="img" class="icon" aria-label="Download" src="download.svg">
Use Caution:
<p id="cap1" class="visually-hidden">Download report</p>
<img role="img" aria-labelledby="cap1" src="download.svg">
Not Recommended:
<img alt="Download" src="download.svg">
Inline SVG — simple label only
Use inline <svg> when you need control over colours, animation, or internals.
Best in Show — inline SVG patterns (simple label):
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Download</title>
</svg>
<svg role="img" aria-labelledby="dl-title" viewBox="0 0 24 24">
<title id="dl-title">Download</title>
</svg>
Use Caution:
<svg role="img" aria-describedby="dl-title7" viewBox="0 0 24 24">
<title id="dl-title7">Download</title>
</svg>
Not Recommended:
<svg role="img" viewBox="0 0 24 24">
<text class="visually-hidden" font-size="0">Download</text>
</svg>
Inline SVG — extended description (complex/informational images)
Use when the SVG conveys more than a name can carry: diagrams, illustrations,
charts, infographics.
Best in Show — inline SVG with extended description:
<svg role="img"
aria-labelledby="chart-title chart-desc"
viewBox="0 0 400 300">
<title id="chart-title">Q1 2024 Website Visitors</title>
<desc id="chart-desc">
Bar chart. January 12,400. February 15,800.
March 19,200 — highest, up 54% from January.
</desc>
</svg>
Note: Pattern 11 sometimes reads both <title> and <desc> consecutively,
which can sound slightly repetitive. This is acceptable — it did not suppress
or ignore any content in testing, unlike the "Use Caution" patterns below.
Use Caution (all have gaps in at least one major screen reader combination):
For Pattern 10 and 12 specifically: <desc> alone, and aria-describedby
pointing to <desc>, are not reliably exposed across all combinations.
Always use aria-labelledby referencing both IDs (Pattern 11) for complex SVGs.
Animated SVGs — prefers-reduced-motion
Rapid SVG animation is a Critical safety issue if it flashes at 3+ Hz
(WCAG 2.3.1 — Three Flashes or Below Threshold). Vestibular disorders can be
triggered by any sustained motion; seizure thresholds are lower.
Always wrap SVG animations in a prefers-reduced-motion check:
@media (prefers-reduced-motion: no-preference) {
.animated-icon {
animation: spin 1s linear infinite;
}
}
@media (prefers-reduced-motion: reduce) {
svg * {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
For SVG animations controlled via JavaScript (e.g., GSAP, Anime.js):
const prefersReduced = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
if (!prefersReduced) {
gsap.to('.icon-path', { rotation: 360, repeat: -1, duration: 1 });
} else {
element.classList.add('animation-complete');
}
For inline <svg> with <animate> or <animateTransform> elements, use
begin="indefinite" as the default and only trigger the animation when
prefers-reduced-motion: no-preference is confirmed:
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40">
<animate id="spin-anim"
attributeName="r" from="40" to="45"
dur="0.5s" repeatCount="indefinite"
begin="indefinite"/>
</circle>
</svg>
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.getElementById('spin-anim').beginElement();
}
Required: currentColor for Theme Compatibility
<svg viewBox="0 0 24 24" class="icon" aria-hidden="true" focusable="false">
<path fill="currentColor" d="…"/>
</svg>
Hardcoded colour values that do not respond to theme changes are Moderate —
the SVG may become invisible or low-contrast in dark mode or forced-colours mode.
Required: Forced-Colors Support
Only needed when the SVG would lose meaning without it:
@media (forced-colors: active) {
.icon { stroke: CanvasText; fill: CanvasText; }
.accent { stroke: Highlight; fill: Highlight; }
}
Use semantic system colour keywords: Canvas, CanvasText, LinkText,
ButtonFace, ButtonText, Highlight, HighlightText.
Required: Non-text Contrast
Meaningful graphical elements (not decorative) require minimum 3:1 contrast
against adjacent colours (WCAG 1.4.11). This is Moderate if failing —
the SVG is still present, but low-contrast elements may be unperceivable for
low-vision users who do not use forced-colours mode.
Required: Preserve viewBox
Do not remove viewBox during optimisation — it is required for responsive
scaling. Its removal is Minor in most fixed-dimension contexts but will
break fluid layouts.
Do NOT Remove During Optimisation
Optimisation tools (SVGO and similar) may remove elements that are required
for accessibility. Configure your optimiser to preserve:
viewBox
<title> when contributing to accessible name
<desc> when contributing to accessible description
- IDs referenced by
aria-labelledby, aria-describedby, <use href="#…">,
clip-path="url(#…)", mask="url(#…)", filter="url(#…)"
- Internal
<style> elements implementing interaction states, reduced-motion,
or forced-colours rules
Interactive SVGs
If the SVG itself is interactive (clickable regions, toggle buttons):
- Prefer a
<button> or <a> wrapper over raw SVG tabindex — native
elements provide keyboard, ARIA semantics, and AT support for free
- Ensure keyboard activation (Enter/Space for buttons, Enter for links)
- Provide visible focus indication
- An interactive SVG unreachable by keyboard is Critical
What Is NOT Required
- A
<desc> for every SVG — Pattern 8 (<title> + aria-labelledby) is
sufficient when a name alone fully conveys the 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
Definition of Done Checklist
Key WCAG Criteria
- 1.1.1 Non-text Content (A) — Critical if meaningful SVG has no text alternative
- 1.4.11 Non-text Contrast (AA)
- 2.3.1 Three Flashes or Below Threshold (A) — Critical for flashing/strobing animations
- 4.1.2 Name, Role, Value (A)
References
Standards horizon: The 3:1 non-text contrast ratio is likely to change
under WCAG 3.0's APCA model. The current 3:1 requirement remains applicable.
Monitor: https://www.w3.org/TR/wcag-3.0/