Accessible SVGs in React

Koen van Gilst

Koen van Gilst / August 23, 2019

3 min read––– views

tl;dr

If your SVG is just decorative add aria-hidden="true". If not, add a <title> tag to improve accessibility. Read on if you want general browser support (Safari in particular).

With <title> tags you can let screenreader users know what a SVG represents. However, browser support is not quite there yet. So for instance VoiceOver on Mac + Safari does not read titles to the user.

You can use some aria magic to fix this:

export const PlayIcon = () => (
  <svg
    aria-labelledby=“play-id”
    role="img"
    viewBox="0 0 24 24"
    xmlns="http://www.w3.org/2000/svg"
  >
    <title id="play-id">play</title>
    <path d="M8 5v14l11-7z" />
    <path d="M0 0h24v24H0z" fill="none" />
  </svg>
);

The challenge with this approach is that HTML ids have to be unique. And when you have a lot of PlayIcons on your screen, this can get problematic. A way to solve this is to generate some kind of unique id for every icon in the DOM, but to me, this feels like engineering overkill.

Hiding SVGs from screenreaders

Sometimes you want to hide the SVGs entirely from screenreaders, for instance when you have a button that already has a label and the icon is just a visual decoration that does not add any indispensable information. In that case, you can use aria-hidden to make sure it's not picked up by screenreaders (just as an empty string as alt tag (alt="") hides images).

In general, I also won't leave the decision whether or not an SVG should have an accessible title to the parent component. So I ended up with the following React component:

export const PlayIcon = ({ accessibleTitle }) => (
  <svg
    aria-labelledby={accessibleTitle ? 'playid' : null}
    aria-hidden={accessibleTitle ? false : true}
    role="img"
    viewBox="0 0 24 24"
    xmlns="http://www.w3.org/2000/svg"
  >
    {accessibleTitle && <title id="playid">play</title>}
    <path d="M8 5v14l11-7z" />
    <path d="M0 0h24v24H0z" fill="none" />
  </svg>
);

And you would use it like so:

const App = () => (
  <button>
    <PlayIcon accessibleTitle="play" />
  </button>
);

Here's a CodeSandbox: https://codesandbox.io/embed/accessible-svgs-4egiz

And here's the website

Browser support

Browser support for using only a <title> tag is getting better. As of 25th August 2019, it looks like this:

  • VoiceOver + Chrome: WORKS ✅

  • VoiceOver + Chromium (new Edge + Brave): WORKS ✅

  • VoiceOver + Safari 12 + 13: DOES NOT ❌

  • VoiceOver + Firefox: (how do I get this to work, somebody knows?)

  • NVDA + Chrome: WORKS ✅

  • NVDA + Edge: WORKS ✅

  • NVDA + Firefox: DOES NOT ❌

Note that on Firefox the solution with aria-labelledby causes NVDA to read the icon name twice.