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.
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:
- Focused elements should be highlighted;
- Navigation order must be logical;
- 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.
Read more:
- 2.4.7 Focus Visible WCAG requirement;
- Introduction to Focus;
- Never remove CSS outlines.
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:
- 1.3.2 Meaningful Sequence and 2.4.3 Focus Order WCAG requirements;
- Tab order;
- Creating a logical tab order through links, form controls, and objects.
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:
- 2.1.2 No Keyboard Trap WCAG requirements;
- Keyboards Traps.
General reading on keyboard navigation testing:
- Navigate using just your keyboard;
- Keyboard Accessibility;
- Why Keyboard Usability Is More Important Than You Think;
- One of my favourite accessibility testing tools: The Tab Key.
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:
- Focus style on active elements can be tested using screenshot testing techniques;
- Lighthouse accessibility audit can expose the absence of landmark roles and heading semantics;
- But even if you scan your site or its individual elements for WCAG compliance, you will not automatically get the correct navigation order because only you (as a user) know and understand the right order.
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:
- Playwright: Accessibility testing;
- Automated accessibility testing;
- Automated Accessibility Testing Is a Good Start — But You Need To Test Manually Too.
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:
- Open the page;
- Press [TAB] key — check the focused element;
- Press [TAB] key — check the focused element;
- Etc.
For the implementation of this check, you need:
- Playwright or any frontend test automation framework;
- Playwright’s
evaluate()
method to invoke a custom function; - 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.
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.