Automated Accessibility Testing of Keyboard Navigation on Tab

One of the first and easiest ways to start accessibility testing on the site is to navigate through the page using just a keyboard.

Andrey Enin
5 min readMar 25, 2024
DALL-E 3 prompt: make minimalistic illustration of accessible website with keyboard navigation. Do not use any text
DALL-E 3 prompt: make minimalistic illustration of accessible website with keyboard navigation. Do not use any text

Introduction to Accessible Keyboard Navigation

Testing your website’s keyboard navigation functionality will help guarantee accessibility for users who rely on keyboards. Usually, keyboard navigation is performed by pressing the [TAB] key, which moves focus between interactive elements, and pressing [ENTER] interacts with them.

Proper accessible keyboard navigation implementation benefits all users regardless of which disabilities (physical or technical) they may have.

A good keyboard navigation includes several aspects:

  1. Focused elements should be highlighted;
  2. Navigation order must be logical;
  3. Extra elements should be skipped.

All of these aspects are regulated by different sections of Web Content Accessibility Guidelines (WCAG), the foremost of which is the 2.1.1 Keyboard.

Focused elements should be highlighted

This means that interactive elements (buttons, links, inputs) should have a visible and obvious focus style. Unfortunately, many websites remove the focus style by the CSS rule {outline: none;} — that is very sad, and it is often done out of ignorance — that’s not how it’s done.

Default focus outline highlighting
Default focus outline highlighting

Read more:

Navigation order must be logical

For keyboard users, the sequence in which interactive elements receive focus matters. It should follow a logical and intuitive order — typically left to right, top to bottom. Predictable navigation usually starts with the header, moves to the main navigation, any page content, and finally, the footer.

Focus should flow between elements as they are positioned on the page, not jump back and forth. The most common way of broken navigation is to have elements with tabindex attribute of 1 or greater because tabindex should only be -1 or 0.

Read more:

Extra elements should be skipped

Among expected interactive elements, extra/minor/unwanted and/or inaccessible widgets should be excluded from keyboard navigation. Just for this case, an attribute tabindex=-1 is required.

Read more:

The correct layout of the document structure with landmarks simplifies navigation for screen reader users by allowing them to navigate straight through page blocks, for example, to skip repetitive navigation.

Read more:

This point can also include avoiding keyboard traps. This issue mainly occurs on pop-ups and overlays when tabbing loops focus on one interactive component. The user should be able to leave a focused element.

Read more:

General reading on keyboard navigation testing:

Introduction to Automation Testing on Accessible Keyboard Navigation

Unfortunately, there is no silver bullet for accessibility automation. The same applies to keyboard navigation. Each of its aspects needs its own separate approach:

For the first time, tabbed navigation should be done only manually. Afterward, if everything is fine (or when all the bugs are fixed), this case can be automated.

Read more:

Next, I will focus only on [TAB] navigation.

TAB Navigation Order Automation

In my current project, we invested some development time in proper keyboard navigation, with a pretty successful result — it became possible for the user to access almost all functionality by tabbing. But after a few releases, we accidentally noticed that an unintended <div> receives focus. That was a sign that the Tab order must be automatically tested.

The test scenario for this case is pretty simple:

  1. Open the page;
  2. Press [TAB] key — check the focused element;
  3. Press [TAB] key — check the focused element;
  4. Etc.

For the implementation of this check, you need:

  1. Playwright or any frontend test automation framework;
  2. Playwright’s evaluate() method to invoke a custom function;
  3. Document’s activeElement property to get the current element on the page that has focus.

To see how the activeElement property works, open DevTools Console and write the command: document.activeElement (read more about detecting focused elements through the browser’s console).

For further examples, I randomly selected the CERN website. But I suddenly faced with a completely unoptimized Tab order — the user could not get on the main navigation list! Well, finding bugs is not the topic of this article. Therefore, I will have to limit the example to just a toolbar with a logo.

Toolbar’s Tab order on CERN’s website
Toolbar’s Tab order on CERN’s website

The first [TAB] press on CERN’s website receives a special element, «Skip to main content». This is a11y hack — an invisible link for skipping navigation.

The only problem with evaluate() function with document.activeElement command is that it returns a Node:

console.log(await page.evaluate(() => document.activeElement));

ref: <Node>

Thus, we need to refer to the Node’s or Element’s interface for getting data, like innerHTML property (it depends on what you decide to check for the focused elements).

Here is an example of a single step of the first [TAB] press:

await test.step('Press TAB key', async () => {
await page.keyboard.press('Tab');

const focusedOn = await page.evaluate(() => {
const selector = document.activeElement;
return selector ? selector.innerHTML : null;
});

expect(focusedOn, 'Should have correct active element').toBe('\n Skip to main content\n');
});

All subsequent steps are the same as the first except for the expected value.

To avoid declarative enumeration of numerous repetitive steps, it would be better to wrap the test step in a loop through the array of the values of expected active elements.

import { expect, test } from '@playwright/test';

const activeElements = [
'\n Skip to main content\n',
'\n CERN\n <span>Accelerating science</span>\n ',
'Sign in',
'Directory',
'\n <img src="/sites/default/files/logo/cern-logo.png" alt="home">\n ',
] as const;

test('Keyboard Navigation', async ({ page }) => {
await test.step('Open the page', async () => {
await page.goto('/');
});

for (const element of activeElements) {
await test.step('Press TAB key', async () => {
await page.keyboard.press('Tab');

const focusedOn = await page.evaluate(() => {
const selector = document.activeElement;
return selector ? selector.innerHTML : null;
});
expect(focusedOn, 'Should have correct active element').toBe(element);
});
}
});

See the sample code in the GitHub repository, where actions on the page are moved into the page object model.

--

--

Andrey Enin

Quality assurance engineer: I’m testing web applications, APIs and do automation testing.