As an engineer, I'm not good at building accessible applications, yet. But, I want to be - accessibility is good for everyone, thank you Laura Kalbag. I've started to try and apply what I learned in Inclusive Components by Heydon Pickering; but, none if it is second-nature yet. So, when I came across an article on trapping keyboard focus by Yogesh Chavan, I was eager to try and reproduce his teachings in my own coding style. The goal of Chavan's article is to be able to keep focus inside of a modal window such that when the user presses
Shift+Tab to navigate, they never navigate to an element that is outside of the modal.
When a user presses the
Tab key in order to navigate around the DOM (Document Object Model), the "active element" follows either a natural tab order (recommended) defined by the HTML structure; or, it follows an explicit tab order given a set of
tabindex attributes (not recommended). Many elements - such as form controls and anchors - have implicit tab access; but, we can add a
tabindex attribute to any element in order to make it focusable.
tabindex="0"to an element grants it keyboard-based access. Adding
tabindex="-1"does not grant keyboard-based access to an element; but, it does allow the element to gain focus programmatically (by calling
.focus()on it) or via a click.
If a user is tabbing through the elements within a modal window, there is-currently-no native way to prevent the user from tabbing beyond the visual bounds of the modal window and focusing an element that his hidden behind the modal backdrop. As such, to keep the focus within the modal window, we have to monitor
keydown events; and, when such an event would lead to navigation outside of the modal, we prevent it and redirect the focus to the "other end" of the modal such that
Shift+Tab can only cycle through the "local" tabbable elements.
To create this trapping, I'm going to listen for
keydown events inside the given container (ex, a modal window) that include the
Tab key. Then, I'm going to query the container DOM for elements that can receive focus:
- Form controls.
- Anchor tags (with
- Anything with
tabindexwhere the value is not
And, if the
keydown event would naturally navigate the user outside of the container, I'm going to
preventDefault() on the
event and throw focus to either the first or last tabbable element (depending on which direction the tab-navigation is going).
For this exploration, I don't actually have a modal window - I just have a
<p> element that will take and maintain focus:
As you can see, when the demo loads, I'm programmatically applying a
Then, I listen for the
keydown event on the container. Since the
keydown event is a bubbling event, this provides a single point from which we can listen for
keydown events on any elements descendant to the container (including the container itself).
This demo is static. However, in a real-world application, the content of the modal window (the container element) may change depending on the state of the view-model and the user's interaction. As such, I don't want to query the DOM ahead of time since the results of such a query may become obsolete. Instead, I wait until an actual
Tab event fires; and then, I query the DOM for the most accurate set of tabbable elements. At that point, I can safely throw focus to either the first tabbable element or the last tabbable element (depending on the presence of the
CAUTION: The selectors that I'm using in this demo to find tabbable elements are incomplete. There can be a lot more complexity to it if you need a generic and robust approach. Just look at the ReadMe in the Tabbable module to see what I mean. That said, when building for yourself, you can probably start simple and just add complexity as you find that you need it.
As you can see, once focus enters the container element, using
Shift+Tab navigation keeps the focus within the container element's tabbable boundaries.
Managing focus within an accessible application is more complex than trapping
Want to use code from this post? Check out the license.