Keyboard accessibility

Reading time 39 minutes

Contents

Introduction

There are many types of users for whom the keyboard is the only way they can access their computer. It also forms the basis for how other users, such as screen-reader and speech-recognition users interact.

Keyboard accessibility is both fundamental to wider accessibility and one of the easiest things to test when it comes to accessibility. Because most developers will primarily use a mouse when interacting with the end product they are coding it can be all too easy to make some simple errors which have a major impact on our end users.

By being aware of how excluding keyboard users can happen we can improve our code and testing practices.

Whilst this is a simple check to do, it actually covers several WCAG criteria because is such a core accessibility concern.

Why test for keyboards?

7% of working-age adults have a severe dexterity difficulty or impairment (​Microsoft / Forrester report​). In the UK of those who reported an impairment around 25% had a condition which impacted their dexterity. This can result in users prefering keyboard interaction over mouse or touch as it may be less prone to error, more easy physically or less painful.

A woman sits at a desk. She uses a stick held in her mouth to press keys on a vertical keyboard in front of her. A screen sits to one side of the keyboard.
A mouth-wand allows users with limb paralysis use a specially adapted keyboard to navigate and enter data.

Some users will be physically unable to use a mouse and so will need to use a keyboard to navigate your product.

A small keyboard has sunken keys. A hand holds a pointer with an angled end over one of the holes.
This small keyboard has a magnetised pointer and sunken keys to assist users who have severe pain when they move their hand any distance. The magnet, sunken keys and small form factor mean movement is kept to a minimum.

Alongside dexterity or mobility impairments, the majority of screen-reader users will also use a keyboard. So by testing for keyboard accessibility we are already making headway into more complex accessibility testing requirements.

Can we automate keyboard testing?

Out-of-the-box automated testing (such as Wave or axe) will only tell you if something is:​

  • focusable and should not be​
  • focusable and is missing an accessible name​

Some automated tools also provide options to visualise focus order (more on that shortly).

But we need to test much more than that.​

Automated tests are effectively linters. ​They catch the simpler issues (although they can be super-useful).​

You would not ship code with the only testing being a linter. In fact with most of the examples we'll be looking at, automated browser plugins did not pick up the issues we will be fixing.

But all is not lost! We can add some checks to our custom scripted tests to help enforce better accessibility at build time. Read more about improving tests for accessibility.

Set-up for testing on Macs

If you are a Mac user, tabbing to navigate links and form controls in a webpage may be turned off by default. This affects Firefox and Safari. You should check your settings if you are on Mac and using either of these browsers.

How to enable tab navigation on Mac

Firefox

To fix Firefox you need to go make a change in the Mac system settings.

Before OS 13 it is in System Preferences > Keyboard > Shortcuts and check “Use keyboard navigation to move focus between controls”.

For OS 13 on it is in System Preferences > Keyboard and toggle on “Keyboard navigation”.

Mac OS Ventura keyboard settings showing the location of the tab navigation option

Safari

For Safari you need to make a change to Safari's own settings. In Safari > Settings > Advanced, check the option “press Tab to highlight each item on a web page”.

Safari settings showing the location of the tab navigation option

What are we testing?

There are a number of things we are going to be looking for during our testing:

  • skip links - adding these allow keyboard users to jump past the repeated blocks which are present on every page, such as navigation
  • visible focus - while we are navigating can we tell exactly where our focus is at any moment - that is to say if we hit the enter key do we know what would happen?
  • access and operation - can we get to and then trigger the various controls on the page? Are there things we can reach which we should not be able to?
  • focus management - when something is added or removed as a result of user action, are we handling the focus in an appropriate manner so they can continue without interruption?
  • focus order - is the order in which focus happens logical based on the position of the elements or are we jumping all over the screen?

Check different states

Also remember we need to test any different view states. These can be:

  • menus which appear when a button is clicked
  • dialogs or notifications
  • different types of content loaded into the same component

Check mobile viewports

We also need to check the mobile viewport of any page. Sometimes developers assume that a mobile user will be using a pointer (such as a finger or stylus), but external keyboards are a popular addition to mobiles or tablets.

An external keyboard being used with a mobile device.

A desktop browser can also trigger the mobile viewport when zoomed. It is important to also check that any interactions, especially menu and navigation related, are still operable with a keyboard.

The CNN website showing the mobile layout on a large resolution desktop monitor.
This website has triggered the mobile view on a large desktop monitor as the browser page zoom has been used to increase the size of the page by 400%;

Whilst looking at mobile viewports, also check how the page operates in landscape mode. This is often when you may find that a sticky header will overlay content preventing a user from seeing where their focus is.

Testing on mobile devices

Whether testing a native app or a website with a keyboard on iOS you will need to turn on “full keyboard access” under Accessibility in Settings. This will allow you to tab through as you might on a laptop.

On iOS you also have the option of using “keyboard gestures” by using the Tab + G command. This then allows you to make swipe gestures. However bear in mind that not all users will be aware of this and Android phone users (nor laptop users) do not have this option. It is always best to provide keyboard users with buttons to accomplish what other users may do with gestures.

A quick look at navigating by keyboard

When we are using our keyboard, we only want to navigate to things which are interactive. The main way of doing this is by using the tab key. This will place a focus indicator on the element to show us where our focus is and this indicator will move as you navigate between interactive elements. You can use enter or space to activate buttons and enter to activate links once your focus is on them.

A mistake that some people make when it comes to testing for keyboard use is that they assume that the tab key is all that is used. Some components might require arrow keys to be used to move around elements within them.

Radio button groups for example show focus on tab (either on the first or last depending on your direction of navigation) and then you can use space to select that radio or use arrow keys to move up and down the list. This behaviour means that long lists of options can be easily skipped over once we have made our choice.

Some library components such as a tab component may have what is called a roving focus (or roving tabindex) which operates in a similar way to radio buttons - you focus onto the first tab element and then use arrow keys to move from tab element to tab element.

Screenshot of a tab component with 4 tabs. The open tab panel shows a link among other content. Notes show how using a tab key will take the user from the current tab to the link in the tab panel, and how when on the active tab using arrow keys will move focus and select the adjacent tab.

This allows the user to avoid having to navigate through all the tabs of the component to get past it. However this also relies on the user being aware of this pattern so this should be used sparingly, else the user may be confused as to why they cannot tab to an element.

Remember that a user will not need to focus on content to scroll the page - this can be done with other keys (space bar, arrow keys or page up/down keys).

Tools

Testing for keyboards can raise some difficulties for us just as it might for a user.

Enhancing focus indicators

Sometimes the page we are checking may have poor focus indicators which make it difficult to work out where our focus is.

One way around this is to enhance the focus indicators with some css. Usually something like this should be enough:

*:focus {
    outline: 2px solid hotpink!important;
}

This will add a pink outline to each focusable element as you move to it.

I added this functionality into the Pattern Checker browser plugin to make it easy to check:

A product page with a review score highlighted by a pink outline. Above it is a panel from the Pattern Checker plugin with settings to show or hide the outline.
On this product page the review score had no focus indicator. By using the Pattern Checker plugin we can more easily see that the review score is part of the tab order as we move through the page.

Viewing the focus order

Sometimes it can be valuable to be able to see each focusable item on the page or to plot where focus is moving. This is especially true when trying to report an issue. Fortunately there are some browser tools which can help us here.

I'm going to look at 3 tools which show the order in which focus is applied to elements on the page.

All these tools plot focus by adding a numbered tag next to the element to represent the order in which they will be accessed by the user. Some also draw lines to assist you in seeing where the next numbered tag is located.

A page with various buttons and links. An overlay has plotted numbers against each with lines connecting each with the next.
Seeing the focus placement and order can be a valuable test tool. We will use these tools later.

However what they see as a focusable element is slightly different.

Firefox's built in "Show tabbing order" in the accessibility panel shows tab stops. These are just the items which are reached by the tab key.

IBM's Accessibility Assessment plugin shows all focusable items. This includes tab stops, but also items which can take focus in other ways.

Microsoft's Accessibility Insights works slightly differently. It doesn't show all the focusable items in one go. Instead it plots the items as you move through the page. Depending on how you navigate will determine how it plots.

The video below shows each of these in action.

Radio button groups are a good way to illustrate how different tools respond. Only the first radio button recieves focus via tabbing. Once in the radio group other radio buttons are accessed using arrow keys.
Video description

A page is shown in Firefox with two radio buttons in a group, below which are a link and a button.

The context menu is brought up and “Inspect Accessibility Properties“ selected. Developer tools opens with the accessibility panel shown.

At the top of the panel is a checkbox for “Show tabbing order“ which is then checked and numbered icons appear next to items on the page.

The icons are numbered next to the following items in this order:

  1. The first radio button
  2. The link
  3. The button

Firefox is dismissed to show the same page in Chrome.

Developer tools is opened and the IBM Accessibility Assessment panel is selected.

There is a “Scan“ button in the panel which is clicked, generating a report. This allows the “Keyboard checker mode” to then be clicked.

Numbered icons with connecting lines appear next to items on the page.

The icons are numbered next to the following items in this order:

  1. The first radio button
  2. The second radio button
  3. The link
  4. The button

The developer tools is closed and the MS Accessibility Insights extension icon is clicked in the toolbar.

From the extension popup the “Tab stops” option is selected and the popup closes.

Focus indicators show the elements on the page as the user moves between them. As the focus moves, numbered icons appear next to items on the page with lines connecting each icon to the next.

The icons are numbered next to the following items in this order:

  1. The first radio button
  2. The link
  3. The button

The page is reloaded and the MS Accessibility Insights extension rerun and the “Tab stops” option selected again.

This time the focus does not move from the first radio to the link, instead moving from the first radio to the second.

As the focus moves, numbered icons appear next to items on the page with lines connecting each icon to the next.

The icons are numbered next to the following items in this order:

  1. The first radio button
  2. The second radio button
  3. The link
  4. The button

It is strongly advisable that these tools are only used to support manual testing and not as a replacement. Also remember that these tools, just like other automated browser plugins only work on the current state of the page.

These are primarily for keyboard users so are a good place to start.

The benefit of a skip link

A lot of sites will employ these but you may not have seen them as they are generally hidden until they recieve focus to avoid making the top of the page cluttered.

A skip link should be the first focusable element on a page, even before the navigation or logo. This is because their function is to allow a keyboard user to jump over the repeated header elements (which tend to be link-heavy with navigation) and allow them to more easily get to the unique content on each page.

If we didn't provide a skip link like this on each page a keyboard user would have to tab perhaps a dozen or more times when they land on each page in order to access the main content links. Remember that keyboard users may have difficulty in activating a key, so asking them to repeatedly do so when we can remove this need is not acceptable.

How skip links should work

The skip link is an in-page link and will typically take the user to the main element (usually done by temporaily assigning a tabindex="-1" to allow it to be programmatically focused). The next use of the tab key will take the user to the first focusable item within or after the main element. Some sites may take the user directly to the first focusable item which is acceptable also.

A skip link on the Mercedes UK homepage with arrows showing how the user is taken beyond the 9 navigation links.
The Mercedes UK website uses a skip link to take the user to the main element. The next use of the tab key will take the user to the first link inside.

Potential mishaps

Despite being a simple interaction there is scope for issues with skip links, partly because their default state is to be hidden visually so they can be forgotten about when testing.

Skip link text

Because skip links are hidden it is easy for issues with their content to be missed. Check the content makes sense and if offering translations of a page that the skip link translates correctly.

A skip link on the Mercedes UK homepage with the text Skip to main content Main Navigation: Text label for skip to content button
Here the Mercedes UK skip link has not loaded its content correctly.

Skip link appearance

We should check that the skip link is actually shown when it gets focus and sits above other content.

A skip link on the Ryan Air homepage. The skip link has not become fully visible on focus and is only visible as a horizontal line at the top of the page. The Ryan Air skip link as it should look - a yellow button at the top of the screen has focus. The button text is Skip to main content.
The Ryan Air homepage skip link has an issue with its styling (a css z-index problem) causing it to be only partially visible. The second image shows how it should look.

Skip link functionality

Our final check is to make sure that when clicked, the link actually takes the user down the page and focus is moved there.

It might be that the target of the link is missing or has been mis-spelled.

The 2024 Olympics website had a bug where the skip links (there were several) scrolled the page but did not set focus. This meant the skip links were making the situation worse as they just added more tab stops for the user to navigate.
Video description

The 2024 Olympics homepage. A "skip to main content" link appears top left.

The link is clicked and the page scrolls.

Another skip link appears replacing the first. This one is labelled "Skip to language selection".

The same happens - the page scrolls but focus does not move from the link.

Three more skip links appear one after the other as focus moves to each in turn, each with the same effect.

The focus now moves to the primary navigation links.

Can I see where my focus is?

Keyboard use relies heavily on being able to see where the focus currently is. If this is taken away by css or obscured (or just not added) it can make it very difficult to work out where you are on the page. Making focus states really obvious helps especially when focus can jump across large sections of the page.

As a quick test, go to the page in question and close your eyes. Hit the tab key several times. Can you see immediately where the focus is? If not then the focus indicators need some work.

Providing good focus indicators

Not having a focus indicator is like not having a mouse pointer for a mouse-user. It makes it a lottery as to what you will trigger when you click. Good focus indicators make interfaces immesurably more pleasant and less tiring to use.​

A good focus indicator makes it immediately clear that the item has focus. Remember that some keyboard users may also have poor eyesight.

Here is a video of a user navigating a site which has poor focus indicators - the user is pressing the tab key throughout the video. The focus is very difficult to see as it only appears on a few links and not at all on the primary navigation. (Note there are other issues apparant in the video which we will come to shortly.)

The Thomas Cook site has poor focus indicators. Navigating with keyboard many links have no indicators at all and a few have just thin dotted outlines.

Now let's look at that again, but this time we add some good focus indicators (done using the Pattern Checker plugin). The focus is now immediately visible as the user tabs through the links.

This video shows the same but with forced focus highlights.

If you can't see where the focus went on a page then you need to check if this is just down to a poor focus state or because the focus has gone to content which has been hidden from view. Disappearing focus indicators is something you will often find when content has been hidden off-screen until triggered (like a menu) - we will look at this in a moment.

Potential mishaps

Visual appearance

The most obvious issue with focus indicators is their lack of visibility. As we have already mentioned a good focus indicator is very clearly different from the non-focused state.

A green button with white text and the word unfocused above alongside an idential one with the word focused above A green button with white text and the word unfocused above alongside an identical one with a faint dotted outline and the word focused above A green button with white text and the word unfocused above alongside a yellow button with black text and a thicker border and the word focused above
From left to right: a button with no focus indicator; a button with a poor focus indicator; a button with a good focus indicator.

Focus indicator clarity can be provided in several ways - via the addition of a border or a background color change for example. However it is applied there should be a good colour contrast between the two states so users with vision impairments can easily percieve the difference.

Finally check your design in Windows High Contrast (WHC) to ensure that focus is still visible where expected. Note that WHC exposes native HTML buttons and links differently to ARIA-based ones, including focus states. So it is generally wise to use native HTML where possible.

Focus obscured

Focus indicators should not be hidden from the user by other content sitting over it. If other content is obscuring the focus indicator then the user will not be able to see either see that content or where their focus is. There are a few instances where this can happen.

Focus obscured by non-modal overlays

A non-modal overlay is one where the rest of the page can still be accessed without having to dismiss the overlay first.

A common use of these is in site menus. However drop-down or fly-out menus can become a hinderance to users when they allow the user to tab through them and onto content which sits behind them without the menu being removed.

Where a menu allows the user to interact with the rest of the page when it is open, the menu should either:

  • be closable via the Esc key when focus is outside the menu so the user can dismiss it without having to backtrack to the close trigger​
  • be auto-closing once focus exits the overlay​ - for example by moving to an adjacent link outside the menu

The Tesco main navigation has drop-down menus which cannot be dismissed with the Esc key and persist when focus moves off them, resulting in the user's focus being hidden from view. Due to the poor focus indicators on the site I had to force focus indicators to show this issue in practice.

Without these considerations a user will be forced to try to go back to what triggered the menu in an attempt to dismiss it.

Focus obscured by modal overlays

Modal overlays are different from non-modal ones in that they need to be closed before the user can interact with the page which launched it. This might be through choosing from a set of options or by using a close button.

These type of overlays communicate this by either taking over the entire viewport (in a lightbox type pattern) or by adding a semi-transparent backdrop between the modal and the page, effectively focusing the user attention on the modal.

Just as with non-modal overlays the issue with modal overlays can be poor coding which allows the user to access content sitting behind them and then be unable to see where their focus is.

Two images of an ecommerce product page. The first shows the prodcut images in a dialog, the dialog background is slightly transparent and a pink focus indicator can be seen on the page behind the dialog. The second image shows the page behind with the focus indicator clearly visible on a button. Text states as the user tabs their focus moves out of dialog and onto items behind it.
This modal overlay (made slightly transparent here to help illustrate the issue) allows the user to tab to content behind it. As the modal cannot be dismissed with the Esc key, the user will have to tab until they find the close button (difficult as the user will not be able to see their focus indicator behind the modal) or reload the page.

With a modal dialog the expected behaviour is that focus is restricted to the dialog in the same way as a mouse user cannot access the content behind it. For a keyboard user this would prevent a user from tabbing out of the modal and onto the rest of the page.

The dialog element will do a lot of this for you. It will:

  • make the page behind the overlay inert (so the user cannot interact with it)
  • add the Esc key functionality
  • add a backdrop
  • place focus on the first focusable item in the dialog

All with minimal effort for the developer.

Focus disappearing

As well as focus disappearing due to other elements appearing in front of them, a common occurence is for focus to drop out of view entirely. This is most often the result of focusable elements which have been visually hidden still being reachable by keyboard.

A keyboard user will only ever want to access controls which they can see. A simple rule-of-thumb is if the user cannot see it when it has focus, then it should not have received focus.

Let's look at an example. This is part of a main navigation for a site. There is a drop-down for a language selector.

A set of links presented horizontally as a site navigation. The first link has the text English and has a downward-facing arrow next to it.

However for keyboard users, instead of being able to tab from the current selection to the next link in the navigation, their focus indicator disappears for half-a-dozen hits of the tab key before re-emerging on the register link.

The same links but with an overlay shoing the focus order. The first link, English with the arrow, is labelled 6. The second link is labelled 13. The intervening numbers are arranged vertically under the English link, but no associated elements are visible.
Showing focus order with the IBM Equal Access Accessibilty Checker browser plugin.

This is because the method of hiding the collapsed menu options was incorrectly applied making the options available all the time.

The same links, but this time the English link has been clicked to reveal a list of different language options in the same location as the numbered overlays from the previous image.

This type of issue is especially harmful because putting content into collapsable components (or hiding off-screen behind a hamburger menu) is often used to hide large sets of options. The worst example I found of this was a filter component which was visually hidden but available to keyboard and screen-reader users and contained over 80 tab stops!

When hiding content from view, whether that is for a mobile-style menu, a set of filters or a drop-down like this, always check that the content inside cannot be tabbed to by keyboard users (or seen by screen-readers).

Scott O'Hara and CSS Tricks both have articles on how to hide content in an accessible way.

Focus wrap-up

If the user cannot see where they are on the page then it becomes very difficult to interact with the content and mistakes will be very likely, leading to further issues.

  • check focused items can be easily differentiated from unfocused ones
  • check the focused item is not obscured by other content, especially components like menus and dialogs

Functionality

There are two main questions we need to ask when looking at keyboard functionality.

  • can the user reach the control with the keyboard?
  • can the control be activated with the keyboard?

Check to see the user can reach any element on the page which they should be able to interact with. Pay particular attention to more complex components and navigation sections. Any component which utlises javascript is especially prone to omitting keyboard functionality.

Using the correct element

The easiest way to ensure a keyboard user can move focus to a control is to use the correct element. This most often means using input, button and a elements.

Whilst in theory a span or div can be made to act like one of the above, it requires a lot of consideration and additional technical complexity. This is often missed and only mouse users are accounted for, or only partially implemented and a poor experience is the result.

Example

The navigation on the Thomas Cook website does not allow keyboard users to access the “My Account” item, as shown by the IBM Equal Access tool below - there is not a focus-order marker for it.

A site navigation. Number overlays mark each link, but none are shown on the My Account option.

Let's take a look at the code for the “My Account” option.

/*My Account*/
<tc-my-account-navigation-open class="⭐️umd9c1-1">
    <div class="tc-menu-toggle-icon"></div>
    <div class="tc-menu-toggle-name">My Account</div>
</tc-my-account-navigation-open>

Now let's compare it to the ”Wishlist” one which sits alongside. This one does take focus.

/*Wishlist*/
<tc-my-account-wishlist-icon class="⭐️umd9c1-1">
    <a href="/my-account/wishlist" class="wishlist-heart">
        <img src="heart-love-outline.svg" alt="Wishlist">
    </a>
</tc-my-account-wishlist-icon>

We can see the “My Account” one is made up only of generic elements (divs), whilst the wishlist is a link (a):

Generic elements (such as span and div) do not take focus, meaning our keyboard user cannot access them. Moreover, whilst a screen-reader user will be able to read the content, they will not know that it is clickable. This is because the elements which have that generic role will be announced purely as text.

The fix

We can fix these issues with a two step approach.

The first is to use the correct element for the job at hand. For this example I will use an a which will allow for a fallback in case javascript is not available and take the user to the account page. We can then progressively enhance this link to open the account menu when javascript is available.

Important to note that an a element without an href is not a link and cannot be focused. So if this was not going to be progressively enhanced I'd opt for a button instead.

I have also added two ARIA attributes on the link to tell screen-reader users that this link will trigger a same-page menu. As we wouldn't want these on a link which will cause a page load we should only add these as part of the javascript-applied progressive enhancement which also adds the event handlers.

This is the code as it would be rendered:

<tc-my-account-navigation-open class="⭐️umd9c1-1">
    <a href="/my-account/" aria-haspopup="true" aria-expanded="false">
        <div class="tc-menu-toggle-icon"></div>
        <div class="tc-menu-toggle-name">My Account</div>
    </a>
</tc-my-account-navigation-open>

With our change in place the navigation now allows keyboard users to access it:

The same navigation now shows the focus overlay indicating that the My Account link is included in the focus order.

Our next step would be to ensure that the keyboard user can then trigger the item to open the menu:

The my account link has been actived and a popup menu appears providing options for the user.

and make sure that our new aria-expanded attribute is updated to true when the menu is open.

This issue is more widespread than you might think. Here is another example, this time the Premier Inn main navigation is entirely unavailable to keyboard users.

A screenshot of the top of a Premier Inn page showing the navigation and and overlay indicating the tab stops. The primary navigation options are not showing as being in the tab order.

If we take a look at one of those navigation links which does not work for keyboard users we can see a similar story.

<div class="css-1wgzkpb">
    <div id="popover-trigger-:r1:" 
        aria-haspopup="dialog" 
        aria-expanded="false" 
        aria-controls="popover-content-:r1:" 
        class="css-jls80j">
            <p class="chakra-text css-1tk9twk">
                Discover Premier Inn
            </p>
    </div>
</div>

As with the previous example we can see the elements are not focusable. Unfortunately whilst they have added aria attributes, they have omitted the most basic functionality. As these aria attributes are not permitted on generic elements these will not be communicated to a screen-reader.

Again, this is a simple fix and can be done by updating the code to use the correct element.

<div class="css-1wgzkpb">
    <button id="popover-trigger-:r1:" 
        aria-haspopup="dialog" 
        aria-expanded="false" 
        aria-controls="popover-content-:r1:" 
        class="css-jls80j">
            <p class="chakra-text css-1tk9twk">
                Discover Premier Inn
            </p>
    </button>
</div>

Tabindex

You may have come across the tabindex attribute. The order in which a user tabs through a page's HTML elements (links, buttons and form controls for example) is called the ”natural tab order”.

Very occasionally you may need to add an element to that tab order which would not normally be there. In this case you can use a zero value in the tabindex attribute to do this.

Note. Tabindex should only be added to elements which need to be focusable in order for them to be interacted with. You also do not need to add tabindex to an element which can already take focus (such as a button) - this is redundant.

When to use tabindex 0

For example here we have two buttons with a div element between them. A user would tab from the first button to the second:

<button>One</button>
<div>I'm plain text</div>
<button>Two</button>

But if we add a tabindex to the div element:

<button>One</button>
<div tabindex="0">I'm plain text</div>
<button>Two</button>

The div will now be included in the natural tab order. The user will now tab from the first button, then to the div, then the second button. As it stands this is not useful or accessible but it demonstrates the principal.

This is something you should only very rarely need to do. Whilst the div can now take focus it is neither announcing as a button or link to screen-readers and will not respond to keyboard clicks. You can start to see why it is always a good idea to use the correct element for the job.

But there are occasions where it can be helpful where no existing HTML element is appropriate. For example, creating a scrollable container (such as a div) will necessitate the container to be focusable to allow keyboard users to scroll the contents. Focusable controls should also have an accessible name for assistive technology and as an aria-label can only be applied to certain roles the div will need to be promoted to something like a role="region". As you can see this process has several ways of being incorrectly applied and causing more barriers.

Tabindex can also take two other types of values. A negative (-1) will allow the element to be programmatically focussed (using javascript) without being added to the natural tab order. This can be occasionally useful, but again rare.

The other value is a positive integer. These should be used even more rarely than our first example as they entirely usurp the natural tab order. This can cause a lot of confusion for users as well as being problematic to maintain in a codebase.

Tabindex: it rarely pays to be positive

How styling can impact functionality

Even when we think we have covered all the bases and have progressively enhanced our interface, it is important to test!

Below we have a product view with various links and buttons. Here I have used the ARC Toolkit's tab order highlight option which works in the same way to the other tools we have used so far.

A product view. Links and buttons have numbers assigned by the tab order plugin. The size options which are represented by the size name - XS, S, M, L and XL - in boxes do not have numbers assigned indicating they are not keyboard accessibe.

The sizing options are not showing as focusable items (confirmed by actual keyboard usage). Let's take a look at the HTML for them:

<label title="XS" for="XS" class="option_field">
    <input value="XS" type="radio" id="XS" name="SingleOption" class="list_option">
    XS
    <span class="checkmark"></span>
</label>

Everything looks ok here as an input has been used which should be focusable. The issue actually lies in the CSS used to hide the radio button:

.option_field input {
    display: none;
}

The use of display: none is the issue. This will remove the input from the rendered page. This then means there is effectively no input on the page to get focus.

This also has a knock-on effect for screen-reader users. No input in the page here means the label associated with it then becomes plain text. Screen-reader users will not be informed the sizes are selectable or be able to select one (just like the keyboard users).

To a screen-reader user the “XS” option will simply be announced as “XS”. The sold out size “XL” will also be read out just as “XL” - as the strikethough is not picked up by screen-readers and the disabled attribute on the input is unavailable as the input is not rendered.

That's a lot of inaccessible effects from some CSS!

The accessibility fix is simply amending the CSS to keep the input in the render but still hide it from sight:

.option_field input {
    position: absolute;
    opacity: 0;
}

Keyboard access is restored without affecting the visual appearance of the component:

A product view. Links and buttons have numbers assigned by the tab order plugin. The size options which are represented by the size name in a box now have numbers assigned indicating they are also keyboard accessibe.

Now screen-reader information also improves.

The “XS” option is now announced as:

XS, tickbox, unticked, 1 of 5

and the “XL” option is announced as:

XL, dimmed, tickbox, unticked, 5 of 5

A screen-reader user now can tell which sizes are sold out, how many sizes there are and which one they have selected.

We would then need to ensure that focus indicators are being applied and actioning the inputs with keyboard has the desired effect.

Reachable but not operable

It is also possible to have a control be reachable via keyboard, but then not be able to action it.

This is normally due to an issue with keyboard events not being handled. When a click handler is added to a link or button element this will include event handlers for keyboard too. However when a click handler is added to a div or span (or other non-native control element) we need to add specific event handlers for keyboard ourselves.

One thing to be aware of is the difference between keyboard clicks and screen-reader clicks and why a screen-reader may be able to trigger something which a keyboard user could not.

When you press Enter on an element with a screen-reader, the element receives a (synthetic) click event rather than a keypress event.

So if your element is only listening for an onClick event you may well be able to access it using a screen-reader, but keyboard users will be left with nothing.

The Asda main navigation has a “More” link which for mouse users opens a drop-down with additional links.

Asda navigation. A link labelled More has opened a drop-down containing nearly a dozen more links to departments and services.

For keyboard users, whilst they can seemingly reach this, no amount of key pressing will open this menu.

The More link with a blue outline focus indicator on the arrow icon which sits to the right of the text.

Let's examine how this control is coded:

<a class="sub-nav" id="nav-toggle">
    <span class="submenu-hamburger-label">More</span>
    <span role="button" 
        tabindex="0" 
        aria-label="More, list 11 items">
        <svg>...</svg>
    </span>
</a>

We have an a element without an href. Inside this is a span with role="button" and tabindex="0" attributes.

So what is happening? Well, as the a element is missing an href it is not classified as a link and nor will it capture focus. This means the keyboard focus skips past that and lands on the internal span thanks to its tabindex attribute having added it to the natural tab order.

The issue for the keyboard users is that the javascript event handlers on the span are only accounting for a click event. As we have already mentioned, on a span (even with a role="button") this will not include keyboard events.

The fix for this is to change the focus point from the span to the a element. Alternatively additional keyboard event listeners could be added, but we should always be using the correct element where possible. We also really want to remove the confusion of the nested controls and the repetition of text which that brings.

Let's remove the following attributes from the span:

  • role - as we no longer want this to pretend to be a button
  • tabindex - as we no longer want this to get focus (we will be using the link)
  • aria-label - as it was mis-representing this as a list when it had its role set as a button, and we no longer need this with our changes

We should also add some ARIA (aria-expanded and aria-haspopup) to help screen-reader users determine how this operates (which will need to be updated when the menu opens).

This results in the following:

<a class="sub-nav" id="nav-toggle" 
    href="#"
    aria-expanded="false" 
    aria-haspopup="true">
    <span class="submenu-hamburger-label">More</span>
    <span>
        <svg>...</svg>
    </span>
</a>

We could probably tidy this up some more and ideally I'd want to use a button as it is not really a link, but this is a definite improvement.

The More link with a blue outline focus indicator around the text and icon and the menu open below it.

Now the focus goes to the link (and is much larger and more visible as a result) and a keyboard user can action it. Additionally a screen-reader user gets the information they should.

Read more about ARIA and accessible names.

Worth noting that although this is a change to markup and that it greatly improves not only keyboard and screen-reader accessibility, there does not need to be any change in how the component looks to the user.

Functionality wrap-up

  • use native HTML controls (inputs, links, buttons) as a first choice when you expect a user to click on something
  • make sure a elements have an href even if you are using javascript to handle clicks
  • if you need to use javascript make sure you cater for keyboard as well as mouse activation
  • you don't get keyboard event handlers for free with generic roles (div and span)
  • do not hide inputs if you expect the user to interact with them

Improving our tests to check for good keyboard accessibility

Logical tab order

Typically for a left-to-right language such as English, we expect focus order to follow the natural reading order, that is for it to move from left to right as it progresses down the page. If we deviate from this reading order expectation with our focus order it can be disorientating for the user and they have a larger chance of losing sight of the focus indicator.

Tabindex

As we have already touched on, it is possible to manipulate the focus order using the tabindex attribute with a positive number value.

Here is an example of where this can go wrong. On the Tesla Powerwall site, content halfway down the page has been given a positive tabindex.

<button 
    role="tab" 
    aria-selected="true" 
    aria-controls="energy-carousel-panel-0" 
    tabindex="1">
        <span role="heading">
            Store Extra Energy
        </span>
        <p class="tcl-carousel-v2__toggle-panel-copy">
            When your solar system ...
        </p>
</button>

This makes this content the first item which will be focused on the page, even before the logo or primary navigation. But this also means that this section will then be “skipped” as the user moves down the page, making for a confusing experience.

The Tesla Powerwall page with an overlay showing the tab stops. The first 3 tab stops are halfway down the page in a tabbed component, after which the tab order returns to the top of the page.

This is likely a mistake rather than a concious decision. The component in question is a tablist which normally has a “roving tabindex”. Rather than using tabindex="1", this should have been using tabindex="0" for the active tab and tabindex="-1" for the inactive tabs. However it shows the pitfalls accompanying the use of tabindex.

Source order vs visual order

There is another way in which we can inadvertently jumble up the focus order for the keyboard user, and that is by manipulating the visual order of our content.

Keyboard focus always follows the order of focusable items as they appear in the source code. When we apply CSS to that code it can visually alter the placement of content (and those focusable items), but this does not change the order in which they receive focus.

Let's look at a very much simplified example.

Here we have a HTML list of 5 buttons labelled one to five. When we tab through them the focus order follows the visual order from top to bottom, one to five.

Then we add some CSS, in this case display:grid, to reposition the third button at the top of the list. Now when we perform the same test, the focus still follows the same order, one to five, despite the third button appearing at the top of the list.

As you can see the focus follows the source order, not the visual order. This also affects screen-reader users in the same way, but not just for focuable items as screen-reader software reads all content.

Keyboard traps

These are relatively rare, but when found can be a complete blocker for a user. They occur when a user reaches an element or component and they cannot exit it using keyboard commands. They are often the result of javascript event handlers not accounting for people using keypresses to navigate.

For example, the National Express homepage has a journey planner at the top of the page. Below it there is also a lot of additional content, including timetables, booking management tools and more.

The user focus gets caught by the first input of the journey planner. The user cannot exit the input without entering data due to javascript event listeners which move the focus straight back to the input if no entry has been made. They cannot even use one of the options in the drop-down as they are not reachable via keyboard.

A form field below the title choose your journey. The field has focus and has placeholder text of Travel from. It has a drop-down with 3 options intended to make finding the start point easier.
This input prevents the user from exiting it in either direction. The helpful options in the drop-down are not accessible to keyboard users.

Similarly, even if they enter a valid stop they are immediately moved to the "Travel to" field which then prevents the user from moving back to the previous field to make any correction.

This is compounded by the result page where the “Edit journey” option is not reachable by keyboard.

A page listing the different options for a journey with a tab order overlay. The edit journey option and option to view earlier journeys are not numbered indicating they cannot be reached with keyboard.
The “Edit journey” and “Earlier coaches” options (highlighted) are not reachable by keyboard.

This type of failure is simple to avoid if those designing and building them account for the different ways a user may interact with it.

Preventing doubling up

Something which will frustrate a keyboard user is where multiple elments are used to link to the same resource.

Let's take a look at this site navigation.

The navigation area for Asda. Two links have been highlighted, help and find a store. Both have icons to the left of the link text.

Note the “Help” and “Find a store” links which have the icons alongside? For a keyboard user there are two tab stops for each of those:

The Asda help and find a store navigation links in more detail and with a tab stop overlay shown. The icons and links are separate tab stops, so each link has two tab stops.

This is not ideal as we are asking the user to do twice the work they should. So why is this happening? If we take a look at the code we can see:

<a href="http://storelocator.asda.com">
    <span role="button" tabindex="0" aria-label="">
        <svg class="asda-icon">...</svg>
    </span>
    Find a store
</a>

Here we have the link, but inside is a span which has been made into a button using ARIA (unfortunately the latter is also missing an accessible name). There is no need for this secondary control, so we can simplify this to the following without loss of functionality:

<a href="http://storelocator.asda.com">
    <span>
        <svg class="asda-icon">...</svg>
    </span>
    Find a store
</a>

Which when applied to both links give us a much cleaner tab order:

The navigation links are now showing just one tab stop each with the tab stop encompassing both the text and icon.

Another place where doubling up is often seen is in card components. Here is a typical example with the links highlighted:

A product card showing an outdoor jacket. The overlay shows that the card itself is a link but so is the product name, a wishlist icon and a quick view button.

and the (simplified) HTML:

<a href="...">
    <img src="1827b921f1.jpg" alt="Carbonite Snow Jacket ">
    <h2 class="product-link__title">
        <a href="...">
            Carbonite Snow Jacket 
        </a>
    </h2>
    ...
    <div class="product-price-wrapper">
        €268.99
    </div>
    <button>Quick View</button>
    <a href="#">
        <span class="visually-hidden">Add to Wishlist</span>
    </a>
</a>

We can see the card has been entirely contained by a link to make the whole card clickable. The issue this creates is adding a repetition of links as this is the same link target as the one which wraps the heading. The other issue with a wrapping link like this is also that its accessible name becomes the link's contents - in this case the entire card contents - and is announced to a screen-reader as:

"Carbonite Snow Jacket Carbonite Snow Jacket Carbonite Snow Jacket 1 / 5 2 / 5 3 / 5 4 / 5 5 / 5 €169.99 QUICK VIEW Add to Wishlist"

As the link wrapping the entire card is only meant for mouse users, we can devolve that to a javascript event on the card wrapper instead, triggering the link on the heading. This allows us to then remove that link element. Something like this:

let cardWrapper = document.querySelector(".card-wrapper");
let cardLink = cardWrapper.querySelector("h2 > a");
cardWrapper.addEventListener('click', function(){
    cardLink.click();
});

The functionality of having the entire card clickable will remain for mouse users, but keyboard and screen-reader users will benefit.

Removing this duplication has the potential to remove a lot of unnecessary keypresses for users as these cards are used to display products in grids.

Managing focus when content updates

When content is removed from the page and the user has their focus on one of the elements which is removed it is important that the user's focus is placed elsewhere thoughtfully. If not then it is likely that their focus will drop back to the top of the page. This will be both disorientating but also mean they will need to make their way down to where they were all over again.

This is especially important when we look at things like:

  • closing dialogs
  • deleting items from a list
  • adding new content to a section which causes the section to be re-rendered

Here is an example of how a dialog can be dismissed leaving the user back at the top of the page:

Here the user moves through the flight planning form on the RyanAir website. After entering airport and date information, the number of adults and children is entered using a small dialog.

Once the passengers have been added in the dialog the user clicks the done button to close the dialog. Their focus is then dropped back to the top of the page, instead of back on the form input, forcing them to navigate the form again.

Because the dialog the user's focus was on has been removed from the page (either through CSS or by removing the element itself) the focus drops back to the top of the page. What should happen is the javascript should place the focus back onto the trigger for the dialog, in this case the passenger input. This helps the user orientate themselves and allows them to either edit the data they just entered or proceed to the next control.

Whilst browsers are beginning to assist with focus management, it is essential for us to be mindful of any effects on a user's focus when a page is updated.

Did we WCAG?

Despite this being a very simple accessibility test on the surface, you can see the complexities it can throw up, as well as pointers to how assistive technology such as screen-readers can be impacted.

If we take a look at the WCAG guidelines we can point to a lot of criteria which the issues we have described above could be failed against:

  • 1.3.1 Info and relationships
  • 1.3.2 Meaningful sequence
  • 2.1.1 Keyboard
  • 2.1.2 No keyboard trap
  • 2.4.1 Bypass blocks
  • 2.4.3 Focus order
  • 2.4.7 Focus visible
  • 2.4.11 Focus not obscured (minimum)

Wrap-up

As we have seen with our examples keyboard accessibility is something which is frequently overlooked, despite these examples being from sites which you might expect have thorough testing procedures.

Keyboard accessibility is something which is simple to test and requires no specialist software. It is a fundamental part of making a website usable by many groups of users, and even acts as a pointer towards deeper accessibility issues which can affect assistive technology users.

With no need for specialist software and simple testing techniques keyboard accessibility should be embedded into every team's accessibility processes.