How to test for accessibility with axe-core in Next.js and React

To make your app the best it can be for as many people as possible it needs to be accessible. Automatically checking your app for accessibility issues during development enables fast iteration and helps you to easily fix low-hanging issues.

Lars smiling

Published 9 min read

What is axe-core

axe-core is the world's leading lightweight accessibility testing engine which can be used to automatically test the output of an app against multiple WCAG rules to help pinpoint and fix accessibility issues. @axe-core/react (formerly react-axe) brings the power of axe-core to React. If Axe finds any accessibility issues in your app, it logs them to the DevTools console in your browser.

Using Axe can help you find on average 57% of WCAG issues automagically! Neat!

If Axe sounds familiar, it may be because of other tools such as Google's Lighthouse and Microsoft's Accessibility Insights For Web which also use this same technology under the hood. So rest at ease that you're in good company when using axe-core 😌

Implementations of @axe-core/react

The implementation of @axe-core/react will differ slightly between a typical client-side rendered (CSR) React app and Next.js, so let's cover both approaches. To keep things simple, this example will be using create-react-app and create-next-app as the starting point for each approach.

How to install @axe-core/react

Add the library as a development dependency to begin:

yarn add @axe-core/react -D
# or
npm i @axe-core/react -D
# or
pnpm i @axe-core/react -D

Adding @axe-core/react to a React project

Keep in mind that the approach for other React projects not based on CRA will be fairly similar, you may just need to configure your tooling appropriately.

It's a good idea to keep the project's main entry point clean and readable. To do that, add the code that will initialize the @axe-core/react module to a separate utility function.

utils/reportAccessibility.ts
import type React from 'react'
 
export const reportAccessibility = async (
  App: typeof React,
  config?: Record<string, unknown>
): Promise<void> => {
  if (process.env.NODE_ENV !== 'production') {
    const axe = await import('@axe-core/react')
    const ReactDOM = await import('react-dom')
 
    axe.default(App, ReactDOM, 1000, config)
  }
}
 
export default reportAccessibility

Start off with a type-only import of React import type React from "react" since it's only going to be used for type annotations in this context. Type-only imports always get fully erased so there's no impact on runtime.

The reportAccessibility function takes two arguments: App which requires a React instance from your app's entry point and an optional config which will allow you to do advanced configuration of accessibility rules. In a lot of cases, you will be able to get by fine without passing in a config.

Axe is only going to be used during development, so it's important to ensure that you're not adding unnecessary code to your production bundle! Dynamic imports combined with an environment check guarantee that all the modules are only loaded as needed.

Now head on over to the app's entry point, import the function and call it.

index.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import reportAccessibility from './reportAccessibility'
 
const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
)
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)
 
reportAccessibility(React)

And that's it!

Now start the dev server, open the app and have a look at the DevTools console in your browser for any reported accessibility violations.

Browser screenshot of the Create React App and DevTools console with two moderate accessibility issues

Check out the GitHub repo to see the full implementation, or feel free to skip on to the "Fixing the accessibility issues" section to see how to resolve these accessibility issues.

Adding @axe-core/react to a Next.js project

Again, let's keep the project's main entry point clean and create a separate utility function that will initialize the module.

utils/reportAccessibility.ts
import type React from 'react'
 
export const reportAccessibility = async (
  App: typeof React,
  config?: Record<string, unknown>
): Promise<void> => {
  if (
    typeof window !== 'undefined' &&
    process.env.NODE_ENV !== 'production'
  ) {
    const axe = await import('@axe-core/react')
    const ReactDOM = await import('react-dom')
 
    axe.default(App, ReactDOM, 1000, config)
  }
}
 
export default reportAccessibility

The only difference in this utility is the typeof window !== 'undefined' check which is required to only run the function when the app is rendered client-side. As with the CRA implementation, you should add an environment check along with dynamic imports to avoid bundling @axe-core/react in your production bundle.

Ensure that you're adding this to pages/_app to enable axe to check the entire app for accessibility violations.

pages/_app.tsx
import React from 'react'
import reportAccessibility from '../utils/reportAccessibility'
import type { AppProps } from 'next/app'
 
function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />
}
 
reportAccessibility(React)
 
export default MyApp

That should do it!

Start the dev server, open the app and investigate the output in your browser's DevTools console.

Browser screenshot of the Create Next App and DevTools console with one serious accessibility issue

Have a look at the GitHub repo to see the full implementation, or read on about a simple example of how to resolve this accessibility issue we're getting in the console.

Interpreting the logged accessibility issues

  • Severity: The severity of issues range from minor to critical and indicates how much impact it has on users.
  • Description: A description of the issue and a link to further information on how to fix it.
  • Reference: If relevant, a reference to the element with the issue can be expanded by clicking the toggle.

Fixing the accessibility issues

The approach to fixing accessibility issues will vary depending on the complexity of your app. Keep in mind that these examples are simple, so they can't act as silver bullets on how to approach making all apps accessible.

Do the right thing by researching how to best fix each issue you encounter. Follow the links next to the issue descriptions and read the WCAG 🙏

Fixing the CRA issues

moderate: Document should have one main landmark https://dequeuniversity.com/rules/axe/4.4/landmark-one-main

moderate: Page should contain a level-one heading https://dequeuniversity.com/rules/axe/4.4/page-has-heading-one

Landmarks are semantic sectioning regions on a page like <main>, <nav>, <footer> and more. These regions improve the navigation experience of your app by making the purpose of the content within them clear and they enable assistive tech users to jump directly from region to region.

All content should be contained within a landmark region. The <main> element should contain the core content that is unique to the page, not including content that is repeated across pages like navigation, sidebars and footers.

Each page should contain a single <h1> heading at the start of the main content. This helps screen reader users to create a mental model of the content hierarchy of the page, and easily navigate the page by jumping from header to header.

src/App.tsx
import logo from './logo.svg'
import './App.css'
 
function App() {
  return (
    <div className="App">
      <header role="banner" className="App-header">
        <img src={logo} className="App-logo" alt="React logo" />
      </header>
      <main role="main">
        <h1>
          Edit <code>src/App.tsx</code> and save to reload.
        </h1>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </main>
    </div>
  )
}
 
export default App

You can fix both issues by simply refactoring the App component to wrap the logo in a <header>, the main content in a <main> and the title in a <h1>. Adding ARIA landmark roles with role attributes is a best practice to ensure that the implementation is as robust as possible across all assistive tech.

Browser screenshot of the Create React App and DevTools console showing the HTML

Fixing the Next.js issue

serious: <html> element must have a lang attribute https://dequeuniversity.com/rules/axe/4.4/html-has-lang

When an app doesn't have a lang attribute, it can cause screen readers to mispronounce text, making the entire app impossible to understand in worst-case scenarios! In addition, assistive tech like screen readers will assume that the default language is set by the user, which can cause issues if the person speaks multiple languages or access an app with more than one language.

This issue is serious, and luckily also quite easy to fix!

The simplest way to add a lang attribute to a Next.js app with only one language is to use Next's Internationalized Routing by modifying next.config.js, adding in values for locales and defaultLocale.

next.config.js
module.exports = {
  reactStrictMode: true,
  i18n: {
    /**
     * All the locales you want to
     * support in your application
     */
    locales: ['en'],
    /**
     * The default locale you want to use when
     * on a non-locale prefixed path e.g. `/hello`
     */
    defaultLocale: 'en',
  },
}

Be as specific as possible with your locale identifier and add a region suffix if possible, e.g en-US or en-AU.

Another approach is adding a custom document in a pages/_document.tsx file and modifying the html element directly.

Browser screenshot of the Create Next App and DevTools console showing the HTML

Wrapping up

Setting up @axe-core/react to run accessibility checks in your React or Next.js app during development is straightforward, and a good first step for developers wanting to make the web a better place for everyone.

Making a practice of following this up by checking the DevTools console for any logged issues, clicking the link to learn more, and then implementing the suggested changes will level up your skills, and help you create apps that users return to again and again.

Go to posts