Andrew Welch
Published , updated · 5 min read · RSS Feed
Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.
Making Websites Better through Accessibility
Making your website accessible isn’t just about legal compliance, it also means not turning away customers. Because who wants to do that?
Making your website accessible means making it available to the largest number of people possible. It’s already a Herculean task to get people to your website to begin with, so we certainly don’t want to turn a % of people away due to accessibility issues.
I find that many developers don’t really understand accessibility, or shuffle it off to the “it’d be nice” list on their tasks. With a little education and proper tooling, it doesn’t have to be that way.
I view website accessibility as customer acquisition, as well as building an inclusive website that serves the most number of people as possible
In many countries, website accessibility is also mandated by the law. In the USA, for instance, there is the Americans with Disabilities Act (ADA), which the DOJ has made clear extends to the Internet.
Companies such as Target (see NFB V. Target) and Netflix (see NAD V. Netflix) have been the subject of lawsuits based on this; but it’s also just good business to allow as many people as possible access to your company’s product or service.
If you plan on working on a website for US Federal or State governments (which includes educational institutions), the Website Accessibility Under Title II of the ADA document outlines the accessibility mandates for websites. Even public companies may very well need to comply with the ADA; check out the Does your website need to be ADA compliant? article for details.
I’m just going to assume that you want to make your website accessible for the practical reasons mentioned above that go beyond legal compliance. Like many of the topics that I’ve discussed before, this is all about making the web better.
Both performance & accessibility are not about padding proposals; they are about making websites that are more effective for both our clients, and their customers.
This is also how we should sell accessibility to our clients, similar to how we talked about selling performance in the A Pretty Website Isn’t Enough article. We should build accessibility into our proposals, and integrate it into our design & development workflow. Educate clients on why it is important, and they’ll quickly get on board.
The Personas for Accessible UX article is a super-useful resource for understanding the type of disabilities people may have, and how that impacts their ability to access your website. Take the time to read through the personas listed there, and come back when you’re done.
Really, it’s worth it. I’ll wait.
Link Major Use Cases
So here’s a non-exhaustive synopsis of the disabilities that people may encounter with your website (cribbed from here):
- Vision (blindness, low vision, color blindness)
- Over 6,500,000 adults, most of them between 18 and 64, and over 650,000 children in the US reported issues related to low vision or blindness in 2011.
- About 1 in 10 men are color blind.
- Hearing (deafness)
- About 36 million adults in the US have some form of hearing loss.
- About 2 in 1000 children in the US are born deaf.
- Motor skills (trouble with using a mouse)
- Cognitive and learning issues (autism, dyslexia, ADD, etc.)
- About 1 in 88 children in the US are on the autism spectrum.
- Probably about 20% of people have some form of dyslexia.
- 11% of children 4 – 17 years of age, and about 4% of adults, in the US have been diagnosed with ADD.
In general, it’s estimated that 12 – 20% of people have a disability of some kind.
So let’s do something about it!
Link Pa11y as Tooling for Accessibility
Actually making websites accessible isn’t that hard; the hard part is knowing what you should be doing. While there are some web-based tools out there, I’ve found that the npm package Pa11y to be the most up to date and effective tool for me.
Pa11y is just a CLI layer on top of HTML_CodeSniffer, which enforces the three conformance levels of the Web Content Accessibility Guidelines (WCAG) 2.0, and the web-related components of the U.S. “Section 508” legislation.
So let’s talk about how to use it.
The abbreviation #a11y is often used for “accessibility”, where the number 11 refers to the number of letters omitted from “accessibility”
I’ll state right upfront that I’m no expert on accessibility; if you want to do a deep-dive on accessibility, check out the Getting Started with Web Accessibility & Using Aria resources. But if you use semantic HTML5 elements, use a proper heading hierarchy, and use alt attributes for images, you’re halfway home.
For the rest of it, pa11y gives you a very easy to understand list of what’s broken, and how to fix it. It’s very similar to using the WC3 Validator to check your HTML for correctness, so it’s not hard to get going on a bullet-pointed list of things to fix.
You will need to have Node & NPM (or Yarn) installed in order to take advantage of it, but hopefully you’re already up to speed using these tools. If you’re not, definitely consider getting on the bandwagon, because frontend development is moving inexorably towards them.
So the first thing we’ll do is install a package called npx; this lets you run node modules without having to install them (either globally or as part of a package), either via npm or yarn:
sudo npm install -g npx
sudo yarn global add npx
We’re installing the npx package globally, so we’re using sudo to make it happen; but you can also consider checking out how to install packages globally without sudo if you choose to, or perhaps don’t have sudo access.
Of course, if you like you can add pa11y as a dependency in your project as well, if you prefer to do it that way. You could even install pa11y globally, the same way we did with npx. But I wanted to introduce using npx as a general way to execute Node modules without having to install them.
npx also has a specific benefit in this case: it’ll make sure we’re always running the latest pa11y with the latest rulesets, without having to keep it up to date.
To run pa11y, we can just do:
npx pa11y --standard "WCAG2AA" --ignore "notice;warning" https://nystudio107.com/blog
We’re telling it that we don’t want to see any notices or warnings, but rather just errors. If you want to do a more comprehensive check, you can show the warnings too, by omitting it from the command.
Out of the box, pa11y supports 4 accessibility standards: Section508, WCAG2A, WCAG2AA, WCAG2AAA. The default is WCAG2AA, but you can specify any standard you want via the --standard command line argument.
All of the WCAG2* standards are the same international standard, but with different levels of strictness (A being the least strict, AAA being the most strict). Additional information on WCAG levels can be found in Understanding Levels of Conformance.
The Section508 standard is for US Federal & State government institutions, should you be working on websites for them.
But we’re just going to use the default WCAG2AA standard, which is middle of the road in terms of strictness (and indeed is the standard that WCAG suggests to use), and is used by default if we don’t specify a --standard.
Here’s what the output looks like:
vagrant@homestead:~$ npx pa11y --ignore "notice;warning" https://nystudio107.com/blog
Welcome to Pa11y
> PhantomJS browser created
> Testing the page "https://nystudio107.com/blog"
Results for https://nystudio107.com/blog:
• Error: Anchor element found with a valid href attribute, but no link content has been supplied.
├── WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent
├── #nav-menu > div > div:nth-child(1) > div > div > a
└── <a href="/"><svg xmlns="http://www.w3.org/2...</a>
• Error: This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 2.93:1. Recommendation: change background to #3c3d3f.
├── WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail
├── #nav-menu > div > div:nth-child(3) > ul > li:nth-child(2) > a
└── <a href="/blog" class="nav-link">Blog</a>
• Error: This text input element does not have a name available to an accessibility API. Valid names are: label element, title attribute.
├── WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.InputText.Name
├── #searchbox
└── <input type="text" id="searchbox" placeholder="search" autocomplete="off" class="autocomplete-input">
• Error: This form field should be labelled in some way. Use the label element (either with a "for" attribute or wrapped around the form field), or "title", "aria-label" or "aria-labelledby" attributes as appropriate.
├── WCAG2AA.Principle1.Guideline1_3.1_3_1.F68
├── #searchbox
└── <input type="text" id="searchbox" placeholder="search" autocomplete="off" class="autocomplete-input">
• Error: Anchor element found with a valid href attribute, but no link content has been supplied.
├── WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent
├── #nys-footer > div > div > div > div:nth-child(1) > div > a
└── <a href="/"> <svg class="scaling-svg sub-lo...</a>
• Error: Anchor element found with a valid href attribute, but no link content has been supplied.
├── WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent
├── #nys-footer > div > div > div > div:nth-child(2) > a:nth-child(1)
└── <a class="social" href="mailto:info@nystudio107.com"><i class="icon-mail-alt"></i></a>
• Error: Anchor element found with a valid href attribute, but no link content has been supplied.
├── WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent
├── #nys-footer > div > div > div > div:nth-child(2) > a:nth-child(2)
└── <a rel="nofollow" class="social" href="https://www.facebook.com/newyorkstudio107"><i class="icon-facebook"></i></a>
• Error: Anchor element found with a valid href attribute, but no link content has been supplied.
├── WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent
├── #nys-footer > div > div > div > div:nth-child(2) > a:nth-child(3)
└── <a rel="nofollow" class="social" href="https://twitter.com/nystudio107"><i class="icon-twitter"></i></a>
• Error: Anchor element found with a valid href attribute, but no link content has been supplied.
├── WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent
├── #nys-footer > div > div > div > div:nth-child(2) > a:nth-child(4)
└── <a rel="nofollow" class="social" href="https://github.com/nystudio107"><i class="icon-github-circled">...</a>
• Error: Iframe element requires a non-empty title attribute that identifies the frame.
├── WCAG2AA.Principle2.Guideline2_4.2_4_1.H64.1
├── #Smallchat > iframe
└── <iframe data-reactroot="" style="z-index: 999999999; position: fixed; right: 0px; bottom: 0px; border: 0px; background-image: none; transition: width 200ms cubic-bezier(0.25, 0.25, 0.5, 1), height 200ms cubic-bezier(0.25, 0.25, 0.5, 1); -webkit-trans...
10 Errors
0 Warnings
0 Notices
What it actually does is pretty nifty: it fires up a “headless” browser, renders your webpage in it courtesy of PhantomJS, and then analyzes it for accessibility issues. This is pretty similar to how Critical CSS is generated, as per the Implementing Critical CSS on your website article.
In any event, that’s quite a number of errors, so let’s take them one by one!
• Error: Anchor element found with a valid href attribute, but no link content has been supplied.
├── WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent
├── #nav-menu > div > div:nth-child(1) > div > div > a
└── <a href="/"><svg xmlns="http://www.w3.org/2...</a>
This can be fixed by adding a <title>homepage</title> as a child of the <svg> that is displayed inline in the global site header, so there’s some text for screen readers to parse. Link targets for screen readers should say where the link will take them, or what clicking on the link will do, not describe what the thing inside the link is (a logo, in this case).
Just imagine someone reading the webpage aloud to you, if they said “link nystudio107 logo” you’d have no idea what clicking on it does. If instead they said “link homepage” then you’d get it.
Another way that this issue can also be resolved is by putting text inside of the <a> tag that is invisible to the eye, but screen readers will pick up. We can do this with a <span class="sr-only"> using the CSS from Twitter Bootstrap:
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
That way, screen readers will have something to read!
• Error: This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 2.93:1. Recommendation: change background to #3c3d3f.
├── WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail
├── #nav-menu > div > div:nth-child(3) > ul > li:nth-child(2) > a
└── <a href="/blog" class="nav-link">Blog</a>
This is saying that the contrast ratio between our menu links and the header background color isn’t high enough. It suggests changing it from #58595b to #3C3D3F to increase the contrast ratio. This is to assist with readabilty for people who have vision problems.
Since these are the nystudio107 branding colors, the problem could have been mitigated by applying the tips from the article Tips on Designing for Web Accessibility during the design process.
• Error: This text input element does not have a name available to an accessibility API. Valid names are: label element, title attribute.
├── WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.InputText.Name
├── #searchbox
└── <input type="text" id="searchbox" placeholder="search" autocomplete="off" class="autocomplete-input">
• Error: This form field should be labelled in some way. Use the label element (either with a "for" attribute or wrapped around the form field), or "title", "aria-label" or "aria-labelledby" attributes as appropriate.
├── WCAG2AA.Principle1.Guideline1_3.1_3_1.F68
├── #searchbox
└── <input type="text" id="searchbox" placeholder="search" autocomplete="off" class="autocomplete-input">
This is saying that our <input> element used for site search lacks a aria-label attribute for screen readers. This can be fixed by adding them to our vue2-autocomplete Vue component.
• Error: Anchor element found with a valid href attribute, but no link content has been supplied.
├── WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent
├── #nys-footer > div > div > div > div:nth-child(2) > a:nth-child(1)
└── <a class="social" href="mailto:info@nystudio107.com"><i class="icon-mail-alt"></i></a>
• Error: Anchor element found with a valid href attribute, but no link content has been supplied.
├── WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent
├── #nys-footer > div > div > div > div:nth-child(2) > a:nth-child(2)
└── <a rel="nofollow" class="social" href="https://www.facebook.com/newyorkstudio107"><i class="icon-facebook"></i></a>
• Error: Anchor element found with a valid href attribute, but no link content has been supplied.
├── WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent
├── #nys-footer > div > div > div > div:nth-child(2) > a:nth-child(3)
└── <a rel="nofollow" class="social" href="https://twitter.com/nystudio107"><i class="icon-twitter"></i></a>
• Error: Anchor element found with a valid href attribute, but no link content has been supplied.
├── WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent
├── #nys-footer > div > div > div > div:nth-child(2) > a:nth-child(4)
└── <a rel="nofollow" class="social" href="https://github.com/nystudio107"><i class="icon-github-circled">...</a>
<a rel="nofollow" class="social" href="https://www.facebook.com/newyorkstudio107">
<i class="icon-facebook" aria-hidden="true" title="Facebook"></i>
<span class="sr-only">Facebook</span>
</a>
The aria-hidden attribute causes screen readers to skip the element, and the title attribute gives sighted people a mouseover tooltip. Then the <span class="sr-only"> provides the text for screen readers to parse, using the CSS from Twitter Bootstrap:
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
That’s all there is to it, we now have nice accessible social icons.
• Error: Iframe element requires a non-empty title attribute that identifies the frame.
├── WCAG2AA.Principle2.Guideline2_4.2_4_1.H64.1
├── #Smallchat > iframe
└── <iframe data-reactroot="" style="z-index: 999999999; position: fixed; right: 0px; bottom: 0px; border: 0px; background-image: none; transition: width 200ms cubic-bezier(0.25, 0.25, 0.5, 1), height 200ms cubic-bezier(0.25, 0.25, 0.5, 1); -webkit-trans...
This one, unfortunately, we can’t fix… because it’s coming from the Smallchat library we use to allow site visitors to contact us via Slack. However what we can do is contact the authors of Smallchat, and point out the accessibility issues, so that they can fix them. Then everyone benefits.
Link Automating Accessibility Testing
The cool thing about pa11y being a Node module is that we can integrate it into our frontend workflow, and automate testing. Take for example this a11y Gulp task:
// Process data in an array synchronously, moving onto the n+1 item only after the nth item callback
function doSynchronousLoop(data, processData, done) {
if (data.length > 0) {
const loop = (data, i, processData, done) => {
processData(data[i], i, () => {
if (++i < data.length) {
loop(data, i, processData, done);
} else {
done();
}
});
};
loop(data, 0, processData, done);
} else {
done();
}
}
// Run pa11y accessibility tests on each template
function processAccessibility(element, i, callback) {
const accessibilitySrc = pkg.urls.critical + element.url;
const cliReporter = require('./node_modules/pa11y/reporter/cli.js');
const options = {
log: cliReporter,
ignore:
[
'notice',
'warning'
],
};
const test = $.pa11y(options);
$.fancyLog("-> Checking Accessibility for URL: " + $.chalk.cyan(accessibilitySrc));
test.run(accessibilitySrc, (error, results) => {
cliReporter.results(results, accessibilitySrc);
callback();
});
}
// accessibility task
gulp.task("a11y", (callback) => {
doSynchronousLoop(pkg.globs.critical, processAccessibility, () => {
// all done
callback();
});
});
This piggybacks off of the package.json setup we outlined in the A Better package.json for the Frontend article, and uses the urls we already have in the pkg.globs.critical object from the Implementing Critical CSS on your website article.
We already have a URL to each major template our site uses for our Critical CSS generation, we can use them again to run our pa11y checks on every template on our website!
For this to work, you’ll need to do npm install pa11y -D or yarn add pa11y -D to add pa11y to our project’s package.json, but then we’re good to go!
So now we can just do gulp a11y and it’ll test our entire website for accessibility issues. Nice!
Link Wrapping Up
Just use pa11y the way you use any other auditing tool: you get a list of things to fix, you address them, and then you re-run the test. Lather, rinse, repeat until all of the issues you plan to address are taken care of.
One really nice thing from a best practices point of view is that if we’ve addressed SEO best practices as per the Modern SEO: Snake Oil vs. Substance article, we’ve already taken care of a number of accessibility issues. Both SEO and accessibility want alt tags for images, for instance. Nice.
It makes sense if you think about it; search engines are essentially screen readers. But it’s great to see the convergence of techniques that fall under the best practices umbrella.
That’s it! It wasn’t so bad at all to address these things, and we probably made a big difference to some people visiting our site.
Accessibility isn’t something that’s terribly difficult to address, especially if you integrate it as part of your design & development process, and use tools like pa11y to help you along the way.
As with anything, it’s much easier to build projects with accessibility in mind than it is to retroactively add it. So make it part of your design & development process.
It’s also worth noting that Pa11y also has some other cool projects based on it, such as Pa11y Dashboard and Pa11y Webservice for more automated monitoring of your website’s accessibility. If you prefer GUI tools, check out Chrome Accessibility Developer Tools and HTML_Codesniffer.
Go make some inclusive websites!