{ Michael Gale } | Web Developer - Melbourne, Victoria

Using aXe to find accessibility issues on my SPA website

5 “easy wins” for more accessible web pages

I saw Rick Giner give their talk Accessibility in Single Page Applications at Decompress, 2016. I took a lot away from it despite the fact that I wasn’t really building single page applications at the time. However, perhaps more than I realised - I have been enforcing rules from memory from this talk.

Watching it again recently, I noticed my memory of certain details from the talk were a little hazy. I figured it would be fairly dishonest of me if I didn’t investigate my website, download aXe and run the pages of my site through it.

Several avoidable mistakes have crept into the site. I thought I would share them, talk about them and explain how easy they were to fix. I hope this will help someone in their decision to take some action on some of their projects.

First, here’s what I tend to get right

I’ll admit I am no accessibility expert. But over the years I have picked up some habits, so I’ll add them here so you can get a feel from where I was at before installing aXe.

  • All images are given a text description for non-sighted users, using alt tags
  • Documents are well-formed
  • Links to articles are in semantic lists, so non-sighted users have a sense of context for the elements around them
  • An appropriate level of contrast between text and background content, although usually this is guess-work
  • Appropriate font sizes and tap-target sizes across devices
  • Appropriate use of landmark tags; header, main, footer to provide context… mostly (more on this below)

Note: For the following examples I have simplified all of the code to convey the core piece of information more easily.

1. ARIA roles used must conform to valid values.

a screenshot from the aXe tool
Issues caused by invalid ARIA roles.

This is a small one, but a good example of how our good intensions can actually end up making accessibility worse. My “site logo” is plain text. The two curly braces that wrap around my name, are decorative pieces only. Their intended role is not to suggest that my name is a piece of code, but to provide a visual metaphor which implies that I, as a person, immerse myself in code. For these reasons, I applied an attribute role="decoration". Problem is, I just made that role up. It’s not a thing. What I was thinking of was “presentation”.

  ❌ role="decoration" or "decorative" isn't a thing.
  ✅ role="presentation" is a thing
<span role="presentation">{</span>
<span role="presentation">}</span>

2. Background images as images

For several images on my site, I’m using a <span> tag with a background image. This is not an inherently accessible pattern, but it allows me to do more detailed crops and ensure consistency of image ratios. I can also apply different crops and image ratio’s at different breakpoints using this technique.

These images are still intended as content, so I’ve set role="img", I also added an alt attribute to the span however, aXe was able to pick up my mistake here.

Just because we’ve changed the intended role of an element, doesn’t mean it will magically inherit the default properties of that element. In this case, I needed to change the alt into an aria-label="..."

<!-- ✅ Best -->
<img src="cat.jpg" alt="a cat with orange fur">

<!-- ❌ Don't do this -->
<span style="background-image:url(cat.jpg);"></span>

<!-- ✅ If you have to use span, add a role and a label -->
<span role="img" aria-label="a cat with orange fur" style="background-image:url(cat.jpg);"></span>

3. Main landmark must not be nested inside another landmark

A screenshot from the aXe tool showing issues caused by nested landmark tags
Issues caused by nested landmark tags

This can sometimes be an issue (for me) when refactoring code from a single html file into split template parts, or maybe when re-using code from other projects. When working too quickly, it can be easy to miss semantic mistakes. For example, sometimes in a Wordpress template, I might close out the header.php with <main> and accidentally open another template with <main> This would lead to a nested landmarks error.

<!-- Landmarks -->
    <!-- ❌ not cool! -->

This was fixed simply by changing the nested tags into regular divs. In the real world, there are also CSS classes applied to these elements. Fortunately I’ve written CSS rules that are not bound to the order of the elements. This way I can move the CSS classes across numerous elements on the page and the styles will persist, no matter what semantic changes need to occur.

This was so easily avoidable, but I just wasn’t looking out for it. Good thing aXe exists!

4. Every ID attribute value must be unique

A screenshot from the aXe tool showing issues caused by ID attributes in HTML
Issues caused by non-unique ID attributes in markup.

Every ID must be unique on the page. However, sometimes when exporting SVG icons from Sketch or Illustrator, we can end up with some messy code. Especially if we’re not particularly strict on naming our layers - or maybe we need to import some icons from a random library. That’s exactly what happened here - in my case the parent and children groups were all given the same ID’s when exporting these SVGs.

<!-- ❌ Multiple copies of the same ID attributes -->

<?xml version="1.0" encoding="UTF-8"?>
<svg id="icon-pencil" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <!-- Generator: Sketch 52.5 (67469) - http://www.bohemiancoding.com/sketch -->
    <desc>Created with Sketch.</desc>
    <g id="icon-pencil" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="icon-pencil" fill="#111111" fill-rule="nonzero">
            <path id="Shape" d="M13.5,0 C14.4469301,5.79827486e-17 15.3125879,0.535005941 15.736068,1.38196601 C16.159548,2.22892608 16.0681581,3.24245588 15.5,4 L14.5,5 L11,1.5 L12,0.5 C12.418,0.186 12.937,0 13.5,0 Z M1,11.5 L0,16 L4.5,15 L13.75,5.75 L10.25,2.25 L1,11.5 Z M11.181,5.681 L4.181,12.681 L3.319,11.819 L10.319,4.819 L11.181,5.681 Z"></path>

I fixed this by manually changing each ID - but this could also be fixed by using something like Jake Archibald’s SVGOMG tool - to strip out all the junk from the files. This way I would end up with the following:

<!-- ✅ Compress SVG images, for faster loading and better a11y! -->

<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg">
  <path d="M13.5 0a2.5 2.5 0 0 1 2 4l-1 1L11 1.5l1-1c.418-.314.937-.5 1.5-.5zM1 11.5L0 16l4.5-1 9.25-9.25-3.5-3.5L1 11.5zm10.181-5.819l-7 7-.862-.862 7-7 .862.862z" fill="#111" fill-rule="nonzero"/>

This code is 43.19% smaller and much easier to read!

A screenshot of the SVGOMG interface SVGOMG, by Jake Archibald. The missing GUI for SVGO.

Another good way to ensure this doesn’t happen again is to install SVGO compressor, an official plugin for Sketch. This uses the same SVGO algorithm to compress an SVG image automatically upon export. It’s best to avoid using the “Copy SVG Code” context menu option in Sketch unless you’re willing to shrink the file manually - since it will copy the un-compressed format. In the week that I’ve taken to write this post, I’ve already adjusted my workflow with this in mind. 💁‍♀️

The landing page of the SVGO Compressor plug-in for Sketch
SVGO Compressor - official plug-in for Sketch

Screenshot of the aXe tool showing hyperlinks without text
Example of a link with no text to describe its action

Each of the social media icons in the footer of my website are simple SVG symbols. There are no labels or tool-tips, currently. This is a dumb move, because a screen reader will see these links and either read out nothing contextual about them, or read the full URL, which is ugly.

This can easily be fixed by wrapping a text label in a screen reader-only class, such as the popular Bootstrap helper class. <span class="sr-only">Twitter</span>. In my case, I can also add aria-label to the SVG icons.

  ✅ Use helper classes to provide context for non-sighted users

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  border: 0;

Were done! Or… Wait, what’s that say?

Screenshot of the aXe tool, showing a success message
No accessibility violations in the current state of the page…

Check in every state.

Well my site doesn’t have accordions, modals or sub-menu’s (not yet) - but I’ll need to use aXe again on any new articles I write, especially if they include any new content types not covered by my existing tests.

Before launching new features I have planned, (such as a light box for images), I’ll want to ensure I have employed best practices while developing and scan any page using that component again before I move to deployment.

There is no tool that can solve every accessibility issue. The next step is to take that advice and check the pages with a screen reading tool - something like NVDA (Windows) or VoiceOver (Mac). I’ve also found Firefox’s accessibility tools helpful. A red flag would be a deeply nested tree of nodes without any lables!

Beyond that, we can generally take steps to improve markup further, even when there are no present issues. Ensuring features are progressively enhanced, exploring tool tips, improving focus management, typography, legibility, contrast, refactoring with better semantics, refactoring with less JavaScript, etc. This is all ongoing work that can be done to improve the website over time.


My website is small. There’s not much complicated state to deal with. It was designed that way to meet my minimum viable product - “text goes on a page”. I did things this way so that I could launch fast, create content and introduce new features as and when they’re needed. In retrospect, my MVP should have included a launch checklist of 0 issues being present on all pages in aXe.

I can totally understand that more complexity would bring about more state and more possible a11y issues to watch out for. But there’s no reason you can’t start small, and tackle issues one by one until a larger app is down to 0 also.

From there… there will still be lots more to do! But with a good starting point like aXe, you may have more of a chance to develop some intuition. Good luck! I know I’ll need it too. 🥳