Idea
As developers, we love to automate boring and repetitive tasks. With the help of good CLI tools, each deployment can be monitored, allowing us to catch issues like application bugs, performance problems, and others early. But what about accessibility$1
There are already several options available on the market, almost ready to use. Each has its strengths and provides slightly different outputs, so an initial analysis will be more than appropriate.
In this article, we’ll cover the implementation of these tools:
- IBM
- AXE
- Google Lighthouse
- pa11y
- Wave
Challenge
The biggest challenge isn't implementing one of the tools mentioned above - not at all. Each of them has very good documentation, so a developer can make it work in minutes.
Even for server-side rendered websites, you already have everything you need because the HTML of a site can be easily downloaded via a simple JavaScript fetch
method.
But what about SPA applications, JavaScript-modified content, or specific application states like when a user sees a modal window that may not be accessible$2
There’s no simple native programmatic approach for getting the full, client-rendered HTML of a React-based application. What we need is a tool that can generate content for us - and even simulate user interactions like clicks.
Regular users already have such a tool - a browser. Developers also have an option - running a browser from the terminal. These tools are usually used for E2E testing or browser automation, but we can leverage them for building our monitoring tool too.
Puppeteer
Puppeteer is Google's JavaScript library for browser automation, offering an easy-to-use API and a wide range of features like page interactions, taking screenshots, and network interception.
Depending on your server setup, using Puppeteer might present challenges. For example, on Vercel or Firebase, I faced major issues until I discovered the package @sparticuz/chromium
, which uses a slightly older Chromium version but works reliably via the CLI.
First things first - let’s install all dependencies:
npm install @sparticuz/chromium@^123.0.1
npm install puppeteer@^24.1.0
npm install puppeteer-core@^22.15.0
For local development, we'll use the puppeteer
package, and for CLI environments, we'll use puppeteer-core
.
The original Puppeteer uses a locally installed browser and is more robust, while puppeteer-core
needs more configuration.
Next, I created a simple JavaScript class to handle browser operations: opening a URL, waiting for rendering, and retrieving the HTML.
import puppeteer, { type Browser } from "puppeteer";
import puppeteerCore, { type Browser as CoreBrowser } from "puppeteer-core";
import chromium from "@sparticuz/chromium";
const isLocal = process.env.NODE_ENV === "development";
export default class BrowserHelper {
static async instance(): Promise<Browser | CoreBrowser | null> {
try {
if (isLocal) {
return await puppeteer.launch({ channel: "chrome" });
} else {
return await puppeteerCore.launch({
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
ignoreHTTPSErrors: true,
headless: true,
});
}
} catch (error) {
console.error(error);
return null;
}
}
static async goto(browser: Browser | CoreBrowser, url: string) {
const page = await browser$5.newPage();
await page$6.goto(url, { waitUntil: "domcontentloaded" });
return page;
}
static async close(browser: Browser | CoreBrowser) {
for (const p of await browser.pages()) {
await p.close();
}
await browser.close();
}
static async crawl(url: string) {
const browser = await BrowserHelper.instance();
if (!browser) {
return null;
}
const page = await BrowserHelper.goto(browser, url);
const siteHTML = await page.content();
await BrowserHelper.close(browser);
return siteHTML;
}
}
Accessibility tools implementation
IBM Accessibility Checker
Documentation
IBM provides an advanced accessibility toolkit along with recommendations on building accessible websites.
Installation:
npm install accessibility-checker
Implementation:
import aChecker from "accessibility-checker";
import BrowserHelper from "../utils/browser";
Google Lighthouse
Documentation
Who doesnt know Google Lighthouse$7 It’s a widely used tool that provides insights into performance, accessibility, SEO, and best practices.
Installation:
npm install lighthouse
Implementation:
import lighthouse from "lighthouse";
import BrowserHelper from "@utils/browser";
const URL = "https://your-url.com";
try {
const browser = await BrowserHelper.instance();
const page = await BrowserHelper.goto(browser, URL);
const results = await lighthouse(
URL,
undefined,
{
extends: "lighthouse:default",
settings: {
output: ["html"],
outputPath: "stdout",
screenEmulation: {
disabled: true,
},
onlyCategories: ["accessibility"],
},
artifacts: [{ id: "Accessibility", gatherer: "accessibility" }],
},
page,
);
await BrowserHelper.close(browser);
console.log(results.report);
} catch (e) {
console.error(e);
}
pa11y
Documentation
pa11y is another excellent tool for running automated accessibility tests.
Installation:
npm install pa11y
Implementation:
import pa11y from "pa11y";
import BrowserHelper from "@utils/browser";
const URL = "https://your-url.com";
try {
const browser = await BrowserHelper.instance();
const page = await BrowserHelper.goto(browser, URL);
const results = await pa11y(URL, {
ignoreUrl: true,
page: page,
browser: browser,
standard: "WCAG2AA",
});
await BrowserHelper.close(browser);
console.log(results);
} catch (e) {
console.error(e);
}
axe-core
Documentation
axe-core offers more lightweight implementation that can be easily combined with tools like JSDOM to perform accessibility checks.
Installation:
npm install axe-core
npm install jsdom
Implementation:
import axe from "axe-core";
import jsdom from "jsdom";
const URL = "https://your-url.com";
try {
const pageContent = await fetch(URL).then((res) => res.text());
const dom = new jsdom.JSDOM(pageContent);
const report = await axe.run(dom.window.document.querySelector("body"), {
ownerDocument: dom.window.document,
rules: {
"color-contrast": { enabled: false },
},
});
console.log(report);
} catch (e) {
console.error(e);
}
Wave
Documentation
Wave is the only tool here that can be accessed directly via an API to check a URL.
However, it comes with limited free credits, and depending on the type of report you want, credits are consumed accordingly.
If you run out of credits - you’ll need to buy more. 🙂
From Wave API documentation:
reporttype=1
(costs 1 API credit) — returns WAVE statistics: number of errors, contrast errors, alerts, features, ARIA elements, and structural elements.reporttype=2
(costs 2 API credits) — includes all of the above, plus a listing of WAVE items by type and count.reporttype=3
(costs 3 API credits) — adds contrast data and XPath locations of each identified issue.reporttype=4
(costs 3 API credits) — adds contrast data and CSS selector values for each issue.
Implementation:
const URL = "https://your-url.com";
const WAVE_API_KEY = "XXX-YYY";
const REPORT_TYPE = 2;
try {
const waweReport = await fetch(
`https://wave.webaim.org/api/request$9key=${WAVE_API_KEY}&reporttype=${REPORT_TYPE}&url=${URL}&format=json`,
).then((res) => res.json());
console.log(waweReport);
} catch (e) {
console.error(e);
}
Playground
To compare results from each tool, I created a small component for website auditing, so you can directly audit your site and compare outputs.