Accessible SVG Headings

Maureen Holland

We recently had a chance to investigate an issue around heading elements missing accessible names. This is particularly problematic for <h1/> elements, which designate the most important heading on a page. The common thread of this problem was our use and misuse of SVG (Scalable Vector Graphics).

Here’s what we learned:

Sometimes a logo is just a logo

SVG is a great fit for logos. Logos are not necessarily a great fit for headings. Before diving into how we could associate SVG headings with accessible names, we had to step back and examine why we chose the SVG as heading in the first place. This allowed us to identify a few pages where we could use a more descriptive text option. A visible text heading is always preferable to an SVG heading.

Test your assumptions

For SVG headings that definitely needed to stay headings, we wanted to find a reliable approach to giving that SVG an accessible name. A developer Slack thread revealed that we had various approaches to consider:

  1. Visually hidden styling on text
  2. SVG <title> element
  3. aria-label attribute
  4. alt attribute

Our test case was a simple page scaffolded with create-react-app, using SVGR:

import { ReactComponent as Logo } from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <h1>Testing SVG labels</h1>
        <h2 data-testid="1">
          <span className="visually-hidden">Pattern 1</span>
          <Logo aria-hidden="true" />
        </h2>
        <h2 data-testid="2">
          <Logo role="img" title="Pattern 2" />
        </h2>
        <h2 data-testid="3">
          <Logo role="img" aria-label="Pattern 3" />
        </h2>
        <h2 data-testid="4">
          <Logo role="img" alt="Pattern 4" />
        </h2>
      </header>
    </div>
  );
}

export default App;

We checked the page against several browsers and MacOS's VoiceOver screenreader. We also tested WebAIM's WAVE extension and jest-dom's .toHaveAccessibleName() test assertion. Results were as follows:

Visually hidden styling:

  • Firefox Voiceover: Pass
  • Chrome Voiceover: Pass
  • Safari Voiceover: Pass
  • WAVE: Pass
  • Jest Accessible Name Assertion: Pass

<title/> tag:

  • Firefox Voiceover: Pass (but not recognized as heading title in accessibility dev tools)
  • Chrome Voiceover: Pass
  • Safari Voiceover: Pass
  • WAVE: Pass
  • Jest Accessible Name Assertion: Fail

aria-label attribute:

  • Firefox Voiceover: Pass (heading title shown with warning icon in accessibility dev tools)
  • Chrome Voiceover: Pass
  • Safari Voiceover: Pass
  • WAVE: Fail
  • Jest Accessible Name Assertion: Pass

alt attribute

  • Firefox Voiceover: Fail
  • Chrome Voiceover: Fail
  • Safari Voiceover: Pass
  • WAVE: Fail
  • Jest Accessible Name Assertion: Fail

We discussed the findings at our next developer meeting. The alt attribute was quickly discarded as it often wasn’t identified as a heading name. The <title/> element and aria-label attribute introduced just enough doubt that we decided to err on the side of caution and use visually hidden styling, which offered the most consistent results.

Pattern 2's heading value is an empty string. Pattern 3's heading value is correct, but there's a warning icon for 'text label' next to it.
Firefox Accessibility Dev tools screenshot from May 10, 2023
1: Testing SVG labels; 2: Pattern 1; 2: Pattern 2; 2: Pattern 3 image; 2: image
Firefox / VO heading list screenshot from May 10, 2023

Do something about it

Honestly, we could have debated this for a week. But we wanted to come out of the developer meeting with an actionable standard, so we’ve placed the .visually-hidden rule on a Notion page to document our current approach to SVG headings (with a reminder to confirm you really, truly, 100% need an SVG heading). It’s not carved in stone and it’s certainly not perfect. But it’s better than what we were doing before and it’s important to celebrate small wins.