Accessibility

Making SVG usable by everyone — labels, descriptions, and interaction

Meaningful images

When an SVG conveys information — a logo, an icon that stands alone, a chart — it must have an accessible name so screen readers can announce what it shows. Without one, a screen reader either skips the element entirely or reads out the raw SVG markup character by character, neither of which helps the user.

There are two solid patterns:

Pattern A: <title> as the first child

The <title> element placed as the first child of the <svg> gives the graphic a name that browsers also surface as a tooltip on hover. Pair role="img" with it so the browser exposes the SVG to the accessibility tree as a single image object — otherwise some screen readers walk into the SVG and read its internal structure instead.

An optional <desc> element immediately after <title> adds a longer description (think: image caption) for users who want more context. The ordering rule is strict: <title> must come first; <desc> second.

<svg viewBox="0 0 120 60" width="180" height="90" role="img"> <!-- title MUST be the first child --> <title>Acme Co. logo: bold "A" in burnt orange</title> <desc> A geometric letter A with a horizontal crossbar, rendered in burnt orange on a light gray background. </desc> <rect width="120" height="60" rx="6" fill="#f5f5f5" /> <!-- Upright strokes of the A --> <path d="M30 52 L50 8 L70 52" fill="none" stroke="#d6452c" stroke-width="8" stroke-linejoin="round"/> <!-- Crossbar --> <line x1="37" y1="36" x2="63" y2="36" stroke="#d6452c" stroke-width="6" stroke-linecap="round"/> <!-- Company name beside the mark --> <text x="80" y="26" font-family="sans-serif" font-size="11" fill="#333" font-weight="700">Acme</text> <text x="80" y="40" font-family="sans-serif" font-size="9" fill="#555">Co.</text> </svg> Acme Co. logo: bold "A" in burnt orange A geometric letter A with a horizontal crossbar, rendered in burnt orange on a light gray background. Acme Co.

A screen reader encountering this announces something like: "Acme Co. logo: bold 'A' in burnt orange, image". The user can also press a key to hear the longer <desc> text. The tooltip appears visually on hover because browsers map <title> to the native tooltip mechanism — a useful bonus for sighted users too.

Pattern B: aria-label directly on the element

When you cannot or do not want to embed a <title> element (for example, the SVG is generated by a third-party library), aria-label on the <svg> tag is a clean alternative. Combine it with role="img" in exactly the same way. There is no tooltip effect with this approach, but the accessible name is fully equivalent.

<svg viewBox="0 0 80 80" width="80" height="80" role="img" aria-label="Green checkmark indicating success"> <circle cx="40" cy="40" r="36" fill="#1f8a4c" /> <path d="M22 40 L35 54 L58 26" fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" /> </svg>

Decorative SVG

Not every SVG carries meaning. A background flourish, a divider swirl, or a purely ornamental border adds visual interest but would only clutter the screen reader experience if announced. The rule is straightforward: if removing the SVG would not change the user's understanding of the page, it is decorative.

Hiding inline SVG with aria-hidden

Add aria-hidden="true" to remove the element from the accessibility tree entirely. Add focusable="false" as well — this is an old Internet Explorer quirk where SVG elements inside an <a> or with tabindex could still receive keyboard focus even when hidden from AT; the attribute prevents that on legacy browsers.

<!-- Purely decorative wavy divider --> <svg aria-hidden="true" focusable="false" viewBox="0 0 300 30" width="300" height="30" preserveAspectRatio="none"> <path d="M0 15 Q37.5 0 75 15 T150 15 T225 15 T300 15" fill="none" stroke="#e07b00" stroke-width="3"/> <path d="M0 22 Q37.5 7 75 22 T150 22 T225 22 T300 22" fill="none" stroke="#f0a500" stroke-width="2" opacity="0.6"/> </svg>

Screen readers skip this element completely. Tab focus never lands on it. Visually it is still rendered normally for sighted users.

Decorative <img> with SVG source

When you use an SVG as the src of an <img> element for decoration, supply an empty alt attribute (alt=""). This signals to assistive technology that the image is decorative — the browser then skips it in reading order. Omitting alt entirely is different: some screen readers will then announce the filename, which is rarely helpful.

<!-- Decorative image: alt="" hides it from AT --> <img src="/media/svg/flourish.svg" alt="" width="200" height="40">

Complex graphics — long descriptions

A simple label is sufficient for an icon. A bar chart, however, may encode data that a sighted user reads by examining the bars. For complex graphics, you need both a short name (what it is) and a longer description (what it shows). The combination of <title>, <desc>, and the ARIA pointer attributes aria-labelledby / aria-describedby makes this robust.

The pattern: give <title> and <desc> each an id, then point the SVG's aria-labelledby at the title's id and aria-describedby at the desc's id. Screen readers then surface both in a predictable order regardless of which ARIA implementation version the browser uses.

<svg viewBox="0 0 220 140" width="300" height="192" role="img" aria-labelledby="chart-title" aria-describedby="chart-desc"> <!-- title MUST be first child; give it an id to reference --> <title id="chart-title"> Quarterly sales — Q1 to Q4 2025 </title> <desc id="chart-desc"> Bar chart showing four quarters. Q1: 40 units. Q2: 75 units. Q3: 55 units. Q4: 90 units. Q4 is the highest-performing quarter. </desc> <!-- Axes --> <line x1="30" y1="10" x2="30" y2="110" stroke="#333" stroke-width="2"/> <line x1="30" y1="110" x2="210" y2="110" stroke="#333" stroke-width="2"/> <!-- Bars (heights scaled: max 90 → 90px) --> <rect x="42" y="70" width="30" height="40" fill="#d6452c"/> <rect x="90" y="35" width="30" height="75" fill="#e07b00"/> <rect x="138" y="55" width="30" height="55" fill="#1f8a4c"/> <rect x="186" y="20" width="30" height="90" fill="#0f766e"/> <!-- Labels --> <text x="57" y="124" text-anchor="middle" font-size="9" fill="#333">Q1</text> <text x="105" y="124" text-anchor="middle" font-size="9" fill="#333">Q2</text> <text x="153" y="124" text-anchor="middle" font-size="9" fill="#333">Q3</text> <text x="201" y="124" text-anchor="middle" font-size="9" fill="#333">Q4</text> </svg> Quarterly sales — Q1 to Q4 2025 Bar chart showing four quarters. Q1: 40 units. Q2: 75 units. Q3: 55 units. Q4: 90 units. Q4 is the highest-performing quarter. Q1 Q2 Q3 Q4

A screen reader announces this as: "Quarterly sales — Q1 to Q4 2025, image. Bar chart showing four quarters. Q1: 40 units…" The user hears the title immediately and can ask for the full description — the same progressive disclosure pattern used for image alt text vs. longdesc, but fully in-document.

For very large datasets (dozens of data points), even a thorough <desc> may not serve users well. In those cases, supplement the SVG with a real <table> element containing the underlying data — assistive technology handles tabular data natively and lets users navigate row by row.

Interactive SVG

SVG shapes like <rect> and <circle> are not natively interactive — they do not participate in tab order, do not fire keyboard events, and carry no role by default. If you add a click handler to a shape and nothing else, keyboard users and switch-access users are locked out completely.

The preferred approach: wrap with <button>

The simplest and most reliable fix is to put the interactive SVG content inside a real <button> element. Buttons are keyboard-focusable by default, fire on Enter and Space automatically, and carry the correct ARIA role without any extra attributes. The button's text content or an aria-label becomes the accessible name.

Note also that text rendered as SVG <path> outlines (outlines exported from a design tool) cannot be selected, copied, or read by AT. Prefer real SVG <text> elements — or real HTML text outside the SVG — whenever the text conveys information.

The fallback: tabindex + role + keyboard handler

When wrapping with a button is not feasible (e.g., deeply nested SVG components), you can make a shape directly focusable with three additions: tabindex="0" puts it in tab order, role="button" tells AT what it is, and a keydown handler that fires on Enter (key code 13) and Space (key code 32) replicates the native button keyboard contract. You must also add a visible :focus-visible style — SVG elements do not inherit the browser's default focus ring the way HTML form elements do.

<!-- Preferred: real button wrapping the SVG control --> <button class="svg-btn" aria-label="Toggle theme" onclick="this.querySelector('circle').style.fill = this.querySelector('circle').style.fill === 'rgb(31, 138, 76)' ? '#d6452c' : '#1f8a4c'"> <svg viewBox="0 0 60 60" width="60" height="60" aria-hidden="true" focusable="false"> <circle class="svg-btn-shape" cx="30" cy="30" r="26" fill="#1f8a4c"/> <path d="M20 30 L28 38 L40 22" fill="none" stroke="#fff" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/> </svg> </button> <!-- Fallback when you cannot use a real button (e.g. shape must be the control): --> <circle cx="30" cy="30" r="26" fill="#1f8a4c" tabindex="0" role="button" aria-label="Toggle theme" onkeydown="if(event.key==='Enter'||event.key===' '){ this.style.fill = this.style.fill==='rgb(31, 138, 76)' ? '#d6452c' : '#1f8a4c'; event.preventDefault(); }" onclick="this.style.fill = this.style.fill==='rgb(31, 138, 76)' ? '#d6452c' : '#1f8a4c'" /> <!-- Remember: also write a :focus-visible rule — SVG shapes do not get the browser default focus ring the way HTML form elements do. -->

Tab to the button and press Enter or Space — the circle toggles between green and red and the status message updates. The red outline defined in the page <style> block appears only on keyboard focus (via :focus-visible), so mouse users do not see an unwanted ring. The SVG inside the button carries aria-hidden="true" because the button's own aria-label already provides the accessible name — exposing the SVG internals separately would cause double-announcement.

Sufficient color contrast

WCAG 2.1 requires a contrast ratio of at least 4.5:1 for text and 3:1 for meaningful non-text graphics (like the border of a checkbox or the bars of a chart). Interactive controls add another requirement: their focus indicator must itself meet a 3:1 contrast ratio against adjacent colors. The red outline used in this demo (#d6452c against a white page) passes this threshold comfortably.

Do's and don'ts — quick summary

  • Do add role="img" and a <title> (as the first child) to every meaningful inline SVG.
  • Do add aria-hidden="true" focusable="false" to purely decorative inline SVG.
  • Do supply alt="" on <img src="…svg"> when the image is decorative.
  • Do use aria-labelledby + aria-describedby with explicit id values for charts and diagrams that need both a name and a data summary.
  • Do wrap interactive SVG in a real <button> — or add tabindex="0", role="button", and a keydown handler — and always write a :focus-visible style.
  • Do use real SVG <text> elements for text that conveys meaning; avoid converting text to path outlines in an export.
  • Don't rely on color alone to encode information — pair it with shape, pattern, or a text label.
  • Don't skip accessible names on SVG icons that sit adjacent to visible text labels — those icons may still be read by AT if not hidden.