Focus on accessibility – accessible menus and modals

In 2018 we commissioned accessibility testing of kahoot.com, real-user testing was enlightening.  The feedback I found most useful was regarding :focus – or more accurately – preventing focus of elements outside an active menu or dialog.

We’ve been working towards making our website and products more accessible. Ensuring our teams are aware of Web Content Accessibility Guidelines (WCAG) and including accessibility testing in our quality assurance process. As part of this effort, we’d like to share our experiences to document our findings and contribute to the tech community.

Our focus problem:

The mobile view of kahoot.com has a menu icon to trigger the off-canvas navigation menu. When clicked the navigation appears and the rest of the webpage is hidden behind.

  • User opens off-canvas menu
  • Focus remains on the menu icon
  • User navigates by keyboard (tabbing)

Focus iterates through the next elements on the web-page, in this case elements on our webpage hidden behind the navigation menu.

In the bottom left of the video above you can observe the path of the focused element changing.

Links on the page behind the menu are being traversed in the focus order.

When a dialog or off-canvas navigation is opened focus of hidden elements should be prevented – the items which can be navigated by keyboard should be the same as those that can be viewed and focused visually.

This becomes a really obvious problem when using a screen reader as every (out of context) element is announced as you navigate.

This issue might be the most common accessibility fail since lack of alt text in images - and I observe it affecting a HUGE percentage of the websites I visit.

The Keyboard Trap

The solution suggested by our testers was to implement a keyboard trap:

  • User opens off-canvas navigation
  • Set focus to the first menu item
  • Trap keyboard within our parent
  • User navigates by keyboard (tabs) past the last element – send focus to the first element within our parent
  • User tabs backwards from first element – send focus to the last element within our parent

You trap focus inside your parent (menu or dialogue), tab past the last element, focus is sent to the first element, tab back from the first element, focus the last element.

It turns out the behaviour above is not correct either, but more on that in a moment…

Easy, right? Actually, it is.

There are a lot of keyboard trap plugins. Some of these seemed pretty complex, so I made a super simple one, which we used instead.

See the Pen
Simple keyboard trap
by Andy Kleeman (@andykleeman)
on CodePen.

So. Happy days?, Job done. I wish!

If keyboard focus can be moved to a component of the page using a keyboard interface, then focus can be moved away from that component using only a keyboard interface, and, if it requires more than unmodified arrow or tab keys or other standard exit methods, the user is advised of the method for moving focus away.

Keyboard traps are not accessible

Our keyboard trap works. It works like a charm, but now we’ve trapped our user within our webpage. The user cannot tab/swipe to reach the browser toolbar and exit our webpage.

I didn’t realise this for some time, actually not until I read BBC’s GEL guidelineswhich I highly recommend reading.

The solution? Based on the above we have two options;

  • Treat the last item in our menu or dialog as the end of the document (preventing focus on anything outside of this). Tabbing past the last item moves focus out to the browsers/Chromes toolbar.
  • Provide the user instruction on how to move focus out of the menu/focus trap. Preferably with a key combination.

The former makes use of the browsers default behaviours and requires no instructions, so that’s the most desirable.

BBCs guidelines suggest using the inert attribute.

HTMLElement.inert - Is a Boolean indicating whether the user agent must act as though the given node is absent for the purposes of user interaction events, in-page text searches ("find in page"), and text selection.

The inert attribute

The inert attribute is not yet supported by all browsers, but a polyfill is available and there’s a video introduction to using it.

Inert would be a great solution for a dialog, since in theory your markup would be structured so that your dialog element was a parent element in terms of hierarchy – you could set your header, main and footer elements to inert – leaving your dialog element placed outside of these active.

– header – marked inert – ignored by the browser
– main – marked inert – ignored by the browser
– footer – marked inert – ignored by the browser
– dialog – only element active, only element the browser acknowledges

This could become more complex for nested markup but this would work for our navigation menu which is only nested within our header where all links should remain active.

Rather than use the inert polyfill we decided to create our own function to set focus context by preventing focus of any element which is not a child of the container passed.

Our focus context solution

The inert polyfill would iterate through all focusable elements within the inert container and prevent focus. Our approach was to pass the container which should receive focus, rather than all those that should not and then set any top level containers (not containing our focus context) to inert.

We prevent focus of any (focusable) element within our page and outside of our focus context by modifying the elements focusable attributes; href attribute becomes data-context-inert-href, form elements become disabled and elements with a tabindex attribute we modify to tabindex=-1.

Elements we modify are marked data-focus-context so we can target the elements to revert these changes later.

See the Pen
Set focus context for Accessibility
by Andy Kleeman (@andykleeman)
on CodePen.

In the video above you can see that after tabbing the last menu item, focus is set to the browsers toolbar. We’ve also updated our focus states to be clearer.

Focus of elements that should be considered inert is now prevented; however we haven’t addressed other interactions such as :

in-page text searches (“find in page”), and text selection.

To achieve this we apply the inert attribute, along with aria-hidden=true to any parent elements outside of our context.

If the dialog is closed we can restore our elements state by calling the function and passing false rather than an element.

It took some time and it took fixing it twice, but I think we’ve now solved our focus problem correctly…