Accessible SVGs

Avatar of Heather Migliorisi
Heather Migliorisi on (Updated on )

DigitalOcean provides cloud products for every stage of your journey. Get started with $200 in free credit!

Scalable Vector Graphic (SVG) is emerging as the preferred graphic format to use on the web today. Are you abandoning the icon font or replacing old pg, gif and png graphics for the well-supported SVG, too? Let’s see how this will impact users of assistive technology (AT) and what is needed in order to ensure a great user experience for everyone.

Graphics and Alternative Text

Before getting started with accessible SVGs, a few quick things to consider regarding graphics, accessibility and alternative text.

1. Does the graphic in question need alternative text?

If a graphic is purely decorative, it does not need to have alternative text.

All <img> tags need the alt attribute to be valid, but the attribute can be left empty (no space) <img src="pathtofile.svg" alt=""> and still validate. 😮

2. What is the context of the graphic and the text surrounding it?

If there text/content surrounding the graphic that provides the alternative text, it does not need additional additional alt text. For example:

See the Pen SVG as img src for figure with figcaption by Heather Migliorisi (@hmig) on CodePen.

What is the most appropriate alternative text for the graphic when it needs an alt attribute (see example 4 for more information)? Depending on the content of the image, it can handled differently:

See the Pen SVG as img src for figure with figcaption by Heather Migliorisi (@hmig) on CodePen.

3. Does the graphic have a function? If so, it should be conveyed to the user

For example, instead of labeling icons exactly as they are represented:

Bad Code Example:

<a href="http://codepen.io/username">
  <img src="codepen_icon.png" 
      alt="CodePen Logo">
</a>

Provide some context for the user:

Good code example:

<a href="http://codepen.io/username">
  <img src="codepen_icon.png" 
  alt="See Picked Pens">
</a>

For more information, please read WebAIM’s, “Alternative Text” article for a comprehensive understanding of accessible graphics and the WAI Web Accessibility Tutorials on Images.

The following examples were developed to work with:

  1. Browsers that support SVG: IE 10+, FF, Chrome and Safari
  2. Most common screen readers used: Jaws, NVDA, VoiceOver (VO) and Narrator

Basic Image Replacement

For the most basic implementation of an SVG, we have following options:

1. SVG as img src

See the Pen SVG as img src by Heather Migliorisi (@hmig) on CodePen.

Let’s check our browser usage stats to see if we need to do anything further. If the site’s users are on latest version (Safari Desktop Version 9.1.1 or iOS Version 9.3.2) or newer we can stop here.

However, if the majority of the users are still on older versions of Safari or iOS, we need to add role="img" to the <img src="linktofile.svg" alt="Pixels, my super cute cat" role="img">.

👏 Shout out to the folks that fixed the Safari/WebKit bug!

This example is fine to use as just a basic image replacement, but it doesn’t allow for us to access to contents of the SVG for either AT or CSS/JS. So, if we want more control over the SVG, we’ll need to inline the SVG directly in the HTML.

2. Inline SVG

Inlining the SVG provides more predictable results and control than if it is added with <use> or <img> because the SVG source is directly available in the DOM which is exposed to the accessibility API that is used by AT.

Let’s take the same basic SVG from the <img> example and say we want to add movement to the eyes. We can do that via JS if we inline the SVG directly into the HTML.

See the Pen Basic SVG – Cat by Heather Migliorisi (@hmig) on CodePen.

Since this SVG does not contain any visible text that describes the graphic, we need to add the alternative text (invisible) by:

  • Inside the <svg>, add:
    • <title>A short title of the SVG</title>
      • must be the first child of it’s parent element
      • will be used as a tooltip as the pointing device moves over it
  • A description can be added if needed
    • a description – note this is not read by narrator (bug filed)

According to the W3C specification, we shouldn’t have to do anything extra for SVGs beyond providing the <title> and possibly a <desc> because they should be available to the Accessibility API. Unfortunately, browser support is not quite there yet (bugs reported for: Chrome and Firefox).

So, to ensure the AT can access the <title> and <desc>:

  • Add the appropriate ID’s to the <title> and <desc>:
    • <title id="uniqueTitleID">The title of the SVG</title>
    • <desc id="uniqueDescID">A longer, more complete description for complex graphics.</desc>
  • On the <svg> tag, add:
    • aria-labelledby="uniqueTitleID uniqueDescID" (use the title and desc ID’s) – both title and description are included in aria-labelledby because it has better screen-reader support than aria-describedby (see tip #4)

One more thing:

  • On the <svg> tag, add:
    • role="img" (so that the SVG is not traversed by browsers that map the SVG to the group role)

Want to add a simple animation, such as eyes blinking?

setInterval(function blinkeyes() {
  var tl = new TimelineLite();
  tl.to(".eye", .4, {scaleY:.01, repeat:3, repeatDelay:.4, yoyo:true, transformOrigin: "50% 70%", ease:Power2.easeInOut});
  return tl; 
 }, 5000);

var master = new TimelineLite(); 
master.add(blinkeyes());

Update the title/description so that it accurately explains the image.

<desc id="catDesc">An illustrated gray cat with bright green blinking eyes.</desc>

See the Pen Simple Inline Accessible SVG Cat – using title and desc by Heather Migliorisi (@hmig) on CodePen.

3. Embed SVG via object or iframe

bug warning For now, I’d steer clear of trying to use <object> and <iframe>. In terms of use with a screen reader, they are not adequate.

Here’s how it went for me:

Choose your method of embedding the SVG and add tabindex="0":

<object type="image/svg+xml" 
    data="/path-to-the-svg/filename.svg" 
    width="50%" 
    tabindex="0">
  <img src="Fallback_image.jpg" alt="alt content here">
</object>
<iframe src="/path-to-the-svg/filename.svg" 
    width="65%" 
    height="500" 
    sandbox 
    tabindex="0">
  <img src="Fallback_image.jpg" alt="alt content here">             
</iframe>

Starting with the final SVG from the inline example, we need to replace role="img" with role="group" on the svg.

<svg id="cat" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" aria-labeledby="pixels-title pixels-desc" role="group">

Here’s where it starts to go down hill…

Add a <text> element in the SVG that contains the content of <title> and possibly <desc> (for NVDA):

<text id="nvda-title">A cute, gray cat with green eyes. Cat illustration by Heather Migliorisi.</text>

Then, add a class to hide the text visually, but keeping the content available for screen readers. We can do this by setting the font-size: 0.

.sr-only { font-size: 0; }

So, you end up with both the <title> (and possibly <desc>) and <text> containing the same content in order to support both JAWS and NVDA.

NOTES:

  • Neither <object> nor <iframe> worked in Chrome. Chrome sees the fallback content, so you could throw alt text in there, which would be a third (or fourth) place to store the same content.
  • JAWS does not read the <text> content (beyond what is in aria-labelledby/describedby)

I recommended (based on browser/screen reader support) to use <img src="svg.svg> if possible. Although it’s not always possible, as object/iframe support interactivity/animation where img often does not, and fallbacks are tricker.

Icons

There are several articles on the topic of replacing the icon font with an SVG. I was curious if using SVG for the icons would allow for easier accessibility implementation. Meaning: if the browsers would support the <title> when you implement the SVG with <use> in the main source SVG. Sadly, nope. But, it’s easy enough to do so with the icon itself and we’ll show you how below.

Once the SVG file is created that contains the icons (I like to use icomoon for this) and it is included in the document (http://codepen.io/hmig/full/OXWMLr/), we need to determine the patterns (icon + link, icon + text, just the icon, ect) that are needed for the site. From those patterns, we can devise the appropriate method of applying alternative text.

To start, the code for icons usually looks like this from an icon generator:

<svg> 
  <title>phone</title>
  <use xlink:href="#icon-phone"></use>
</svg>

Example 1: Standalone Icon, Meaningful

Meaningful icons need alternative text. This method is very similar to the “Basic Image Replacement, Inline SVG Example.”

  • Update the title text to be reflective of what the icon is there for … let’s say it’s showing a service supports mobile devices
  • To the <svg>, add role="img" (because the SVG is not mapped consistently, so it is not always acknowledged by AT. For example, the following does not work: Mac – VoiceOver + Chrome or Safari, Windows – NVDA + FF)
<svg role="img"> 
  <title>Supports Mobile Devices</title>
  <use xlink:href="#icon-phone"></use>
</svg>

Again, let’s check our browser usage stats to see if we need to do anything further. If the site’s users are on latest version (Chrome 49.1) or newer we can stop here.

However, if the majority of the users are still on older versions of Chrome, we need to add an id="xxxx" to the <title> and an aria-labelledby="xxxx" on the <svg>.

👏 Shout out to the folks that fixed the the Chrome bug!

See the Pen Example 1: Standalone SVG Icon, Meaningful by Heather Migliorisi (@hmig) on CodePen.

Example 2: Standalone Icon, Decorative

Decorative icons (icons that repeats the information conveyed by text or do not add significant value) do not need alternative text and they should be hidden from the screen reader. For this example, hide the SVG with aria-hidden="true".

<p>
  <svg aria-hidden="true"> 
    <title>checkmark</title>
    <use xlink:href="#icon-checkmark"></use>
  </svg> 
  Success! Your order went through.
</p>

See the Pen yJYVpa by Heather Migliorisi (@hmig) on CodePen.

Example 3: Linked Icon, no text

For linked icons that are not paired with text, we can use aria-label on the <a> element to provide descriptive, alternative text. Add aria-label="See Picked Pens" the <a> element.

<a href="link" aria-label="See Picked Pens">
  <svg> 
    <use xlink:href="#icon-codepen"></use>
  </svg>
</a>

See the Pen Example 3: Linked Icon, no text by Heather Migliorisi (@hmig) on CodePen.

Example 4: Linked Icon, with static text

Again, for linked icons that are paired with text, let’s use aria-label on the <a> element to provide descriptive, alternative text.

With aria-label on the anchor tag, the screen reader does not announce the text inside of the link. Add aria-label="See Picked Pens" the <a> element.

<a href="link" aria-label="See Picked Pens">
  <svg> 
    <use xlink:href="#icon-codepen"></use>
  </svg>
  CodePen
</a>

See the Pen Linked Icon, with static text by Heather Migliorisi (@hmig) on CodePen.

Example 5: Linked Icon, with dynamic text

Let’s say there’s a dynamic value in the linked text + the icon. Here, we should not use aria-label on the link because then the value of the dynamic text is lost. For this example, we can use spans and the off-screen text method. The numeric value in the span with id=”itemsInCart” is the dynamically added element.

  • Add an additional span with the rest of the alternative text (e.g. “items in your shopping cart”)
  • Add the class="offscreen-text" to visually hide this text
  • Add aria-hidden="true" to the svg
<a href="http://example.com" id="cart">
  <span id="itemsInCart">0</span>
  <span class="offscreen-text">items in your shopping cart</span>
  <svg aria-hidden="true">
    <use xlink:href="#icon-cart"></use>
  </svg>
</a>

See the Pen Example 5: Linked Icon, with dynamic text by Heather Migliorisi (@hmig) on CodePen.

Complete list of the icon examples:

See the Pen Accessible SVG Icons by Heather Migliorisi (@hmig) on CodePen.

Complex images – An Accessible Graph

It’s great that we can use SVGs instead of PNGs and JPGs, especially when it comes to complex web content like a graph. It would be excessive to provide all of this info in the alt attribute, so providing alternative text for this image (as a png/jpg) would be tricky. Instead, we can use an SVG and make all of that text directly accessible!

1. Setting up the file

Order of layers – In Adobe Illustrator, the layers will export in the SVG from the bottom up. This is important because we want to set up the layers in the order we want them to be logically traversed by the keyboard for reading. The “Jaws” group should be listed first in the code, so the Jaws layer is at the very bottom in Illustrator.

Layer Naming – It’s a good idea to name the layers because they will be added as id’s to the exported SVG. Don’t worry if layers are named the same, the id’s will be incremented in that case.

Layer Grouping – It’s important to note how the items are grouped here. Text label + the key item (for color) and the bar in the graph are all contained in a group for each graph variant (Jaws, NVDA, ect). The reason for setting it up this way is for someone using a screen reader for reading comprehension. In some browsers, the user can click the bar and the corresponding text is read out and/or highlighted.

Saving/exporting – As a safeguard, I like to keep two versions of my SVG, one for editing in Illustrator and the other for code editing. So, I do a “save as” for the illustrator version and “file > export > svg” for the cleaner, web version.

Optimization – The last thing before hand-editing the SVG, is to optimize it. Jake Archibald’s tool, SVGOMG, is an excellent tool for this. Add the SVG, then, switch to “CODE” view to see exactly what the feature toggles are doing. Set “prettify” on because we still have to edit the code, so we want it to be readable.

It is best to hold off on hand-editing the SVG (adding accessibility features) until it’s 100% certain the SVG is complete design-wise. Once we start down the road of hand-editing, it can be hard to catch if an editor (Inkscape/Illustrator/etc) has inadvertently changed something that was explicitly added.

Source control – If using a git-based variation of source control (git, SourceTree, ect), commit the SVG. Managing the file through one of these will help catch any funky/unwanted changes if it is opened and saved from Illustrator (or other visual editors) after it was edited by hand because any code (aria-*) Illustrator doesn’t understand, it removes/deletes.

2. Let’s make it accessible

Screen reader traversable – Make sure the SVG is traversable in all browsers by adding role="group" to the <svg>. According to the new SVG spec, they should map to the graphics-document role. However, the spec is still in working draft mode, therefore, the browsers have not implemented that yet.

Title and desc – Since we have text elements in the SVG that acts like the title and description, let’s link them up to the <svg> element with aria-labelledby="graph-title” and aria-describedby="graph-desc".

Content cleanup – Clean up any weird elements that Illustrator may have created. For example, several <tspan>s were added to the following <text> element. A screen reader may read out the individual letters (“J” “a” “w” “s” “- 44%”) instead of the word (“Jaws – 44%”). So, remove the unnecessary <tspan>s wrapped around the individual letters.

Example bad:

<text class="cls-2" transform="translate(345.49 36.45)">
  J
  <tspan x="6.23" y="0">a</tspan>
  <tspan x="14.22" y="0">w</tspan>
  <tspan x="26.51" y="0">s - 44%</tspan>
</text>

Example fixed:

<text class="cls-2" transform="translate(345.49 36.45)">
  Jaws - 44%
</text>

Add a link to the survey – Content-wise, since this graph is based-upon a survey, let’s link to it. For now (it is not a requirement in SVG 2), in SVGs, add xlink: to the href.

<a xlink:href="http://webaim.org/projects/screenreadersurvey6/#used">

For more information on xlink, check out “We’ll Always Have Paris: Using SVG Namespacing and XLink” by Dudley Storey.

Add semantic roles – Add some semantic roles to the groups containing the bars, label and key. Let’s make the group that contains all of these a list because most screen readers will announce the total number of items in the list and what position in the list the current item is:

<g id="bars" role="list">

and the individual groups inside can be listitems:

<g id="Jaws" role="listitem">

Add a label to the list – Provide AT users a little more info about the graph they are interacting with. Add a label to the group containing the list with aria-label="bar graph".

<g id="bars" role="list" aria-label="bar graph" transform="translate(0,58)">

Test and fix – Let’s test it with the screen reader. As expected, the screen reader reads through the title, desc and key list items. But, it also goes through the percentage values on the y-axis, each rect and line.

A quick note on hiding elements (rect, circle, text) from AT in an SVG – The only way to “hide” elements from a screen reader in an SVG is by adding role="presentation" to it. What this does is negates the element’s native semantics from mapping to the accessibility API. If you have multiple items you want to hide, unfortunately, you cannot wrap everything in a <g> and add the role="presentation" to it. The only thing that does is hide that element from the AT, the children of it are still traversable. So, we have to add role="presentation" on each individual element we want hidden. On the bright side, the new SVG Accessibility Spec aims to ease a lot of this burden. Elements, such as shapes without alt text, will be treated as if they had a role of none or presentation.

Hide the shapes/lines – Hide the shape elements from the screen reader by adding role="presentation" to each element.

Hide Text Elements – Hide the confusing text elements (as shown above highlighted in yellow – the 0-50% on the y-axis, the x/y axis lines and the bars in the chart) from the screen reader by adding role="presentation" and aria-hidden="true".

See the Pen Accessible Complex Image – Bar Graph by Heather Migliorisi (@hmig) on CodePen.

Screen reader demo videos:

Interactive images

Even better than accessible charts and graphs are accessible interactive images. Let’s look at a simple timeline infographic. What this breaks down to is: the top “title” text section and the timeline section. From there, the timeline breaks down to time segments that contain a title, image and description.

The inspiration for this example was found on codrops, but I wanted to see if I could make an accessible variation. The adorable cat icons are from iconka.

Let’s add some life to this and animate the time segments. Instead of showing all of the info at once, let’s just show the time and activity title circle. When a user interacts with the time area or activity circle, the rest of the content will be revealed.

1. Setting up the file

First, follow the “Setting up the file” section previously covered in this article. Starting from the web optimized version, let’s skip past the css animation part is set up and dig right into making it accessible.

2. Making it accessible

Style properties were removed in the following examples to simplify the code, but are in the actual demo.

Screen reader traversable – Make sure the SVG is traversable in all browsers, so add role="group" to the <svg>.

<svg id="InteractiveSVG" role="group">

Title and desc – For this example, we can use the text at the top of the SVG <g id="timeline-title"> as the title and link it by adding aria-labelledby to the <svg>.

<svg id="InteractiveSVG" aria-labelledby="timeline-title" aria-describedby="timeline-desc" role="group">

Then, add an id to the <desc> and link it up with aria-describedby on the <svg>.

<desc id="timeline-desc">An Interactive Timeline</desc>
<svg id="InteractiveSVG" aria-labelledby="timeline-title" aria-describedby="timeline-desc" role="group">

Add semantic roles – Add some semantic roles to the groups containing the timeline and time segments. Let’s make the group that contains all of these a list: <g id="timeline" role="list">.

Add a label to the list: <g id="timeline" role="list" aria-label="the timeline, from morning to night">

and the individual grouped time segments inside can be listitems: <g id="play" role="listitem">

Interactive/keyboard accessible – Immediately after each <g> that has the role="listitem" add an <a xlink:href></a> so that it contains the entire group. Currently, this is the only way to add interactivity to an SVG.

Add tabindex="0" to ensure that it is focusable in all browsers.

<a xlink:href="#play-group" tabindex="0" id="play-group"></a>

Fix the semantics of the link – Notice that the link is to itself. This really isn’t a semantic link because it does not link to anything and could confuse screen reader users. So, let’s add a role="img" to signify that it’s an image element instead of a link.

<a xlink:href="#play-group" role="img" id="play-group"></a>

Make the text inside of the time segment accessible – Adding the img role stops the AT from traversing the rest of the elements, so we need to add aria-labelledby with the ids of the text elements in the order they should be read.

<a xlink:href="#play-group" role="img" aria-labelledby="play-time play-text" tabindex="0" id="play-group"></a>

Add hidden descriptive text for the images – Use the <tspan> with an offscreen class, so that visually it is hidden, but it remains in the DOM.

<tspan class="offscreen" id="play-description">A gray kitten tangled in a ball of yarn.</tspan>

Add the ID to the aria-labelledby on the xlink so that it is read.

<a xlink:href="#play-group" role="img" aria-labelledby="play-time play-text play-description" tabindex="0" id="play-group"></a>

Set visual CSS for focus – Setting a visual representation for focus is needed for folks using the keyboard to navigate. I liked how it looked, so I added it to the hover state as well.

a:focus [class*="time-circle"], a:hover [class*="time-circle"] {
  stroke: black;
  stroke-width: 5;
  paint-order: stroke;
}

Add JavaScript for window focus – With SVGs, when you navigate through the link items, the window does not always shift to ensure the element is in the viewport. The reason is some browsers (bug filed and hopefully fixed soon) are scrolling the <svg> element as a whole without regard to the children elements that may be offscreen. So, let’s add some JavaScript to scroll the window to ensure the focused elements are visible.

There’s probably a more efficient way to do this, but this is just a quick example:

$("#play-group").focus(function() {
  window.scrollTo(250,350);
});
...
$("#cuddle-group" ).focus(function(){
  window.scrollTo(250 , 1350);
});

See the Pen Accessible Interactive SVG by Heather Migliorisi (@hmig) on CodePen.

Screen reader demo videos:

SVGs and High Contrast Mode

Another wrench in the mix: Windows and High Contrast Mode for people with low vision use this feature to help with the readability of content. It’s a wrench because, when used, the text and document body may change color when the feature is activated, but elements in an SVG are not updated when a user selects different contrast modes.

image from http://disney.wikia.com/wiki/File:Alice-facepalm.jpg

The good news: There are media queries to handle this.

Examples for how to fix the icon section in the article:

@media screen and (-ms-high-contrast: active) {
  .icon svg {
    /* select a color that will contrast 
       well on black or white because other 
       color modes can be chosen and you 
       need a color that will work with either 
    */
    fill: green;
  }
}

/* black text on white background *.
@media screen and (-ms-high-contrast: black-on-white) {
   .icon svg {
     /* select a dark color that will 
        contrast on black 
        (#fff is too much contrast) 
     */
    fill: #333;
  }
}

/* black text on white background */
@media screen and (-ms-high-contrast: white-on-black) {
 .icon svg {
    /* select a light color that will 
       contrast on white 
       (#000 is too much contrast)
    */
    fill: #efefef;
  }
}

Conclusion

  • Determine if alternative text is needed
    1. If no, hide the image/SVG aria-hidden="true"
    2. If yes:
      1. Add/link up title and/or description to the SVG element
      2. Use roles to add semantic value (e.g. role="list", role="listitem")
      3. Hide graphical and group elements that should not be read w/ role="presentation"
      4. Hide text elements that should not be read with role="presentation" and aria-hidden=”true”
  • Determine if the SVG is interactive
    1. If no – do nothing else
    2. If yes:
      1. Set focus with xlink + tabindex="0"
      2. If the link is not an actual link, add a role to ensure it is semantic
      3. Add JS for setting window focus
      4. Set visual CSS for focus: outline
  • Test with various screen readers + browsers. Test different contrast modes. Test keyboard navigation.

Special Thanks

A huge “thank you” to Amelia Bellamy-Royds and Leonie Watson for double checking some of the examples and pointing out issues! I couldn’t have put this article together without their help.

Bugs filed while working on this article \o/

Microsoft:

Mozilla:

Bugs fixed while working on this article 👏

  • Safari/WebKit bug that required the adding of role="img" to the <img> tag
  • Chrome bug that made you add aria-labelledby or aria-label to the svg in order to have the title read.

Resources