Making keyboard navigation more accessible with JavaScript ‘focus traps’
By: Benjamin Kroll | February 19, 2025 | dev tools and Web accessibility
Tabbing through a web page can be a frustrating experience. The user tabs to access a menu, but with the keyboard's next tap, they’ve moved on to another page element and have to retrace their steps to access the desired content.
For users who rely on keyboard navigation, this can be a major accessibility roadblock. And for other site visitors, it’s just poor UX.
Fortunately, you can implement a fairly straightforward function in JavaScript called a “focus trap” to ensure users don’t leave the page area they’re in without intending to do so.
For example, when a user opens a modal window, a focus trap can dictate the order that keyboard actions move the focus to within the modal until it's dismissed, rather than navigating out of the window.
In this post, I’ll walk through a vanilla focus trap function that I used to create an accessible mobile search element for our customer, the Chinook Arch Regional Library System.
I’ll also discuss a few related issues and provide other examples of how this tactic can improve your site’s accessibility and usability.
Focus traps are essential for accessibility
The Americans with Disabilities Act (ADA) requires that sites be fully navigable by keyboard, so focus traps have become a de facto element of good page design.
In fact, Bootstrap and other development frameworks provide this feature out of the box for their components. So If you’re using a framework, you may not need to add this function to your pages. But it never hurts to expand your hands-on toolset as a developer. For further reading, I’d suggest Hidde’s Blog’s seminal post from 2017. It really spells out the basic principles of the focus trap function.
Avoid ‘keyboard traps’ as you define your focus
Another key issue to understand about focus traps is that you should provide additional accessibility information and tools, when appropriate, to let users exit your focus trap if they want to.
Example: A user pops open a modal, and pressing the [Tab] key now cycles focus in order through the modal elements. When the last element is reached, pressing [Tab] takes the user back to the first element in the focus order. The [Cancel] or [Esc] key is used to close the modal window (again, you can’t expect the user to just use their mouse to click the X icon). Adding aria-labels to help guide the user through this process can be very helpful.
The W3C has issued a success criterion (SC 2.1.2) about avoiding what it calls “keyboard traps,” particularly in cases where you may employ a non-standard keyboard combo to exit your modal, menu, or calendar element. It’s a fairly clear-cut standard, but worth remembering as you employ focus traps for your page elements, particularly if you do it manually.
The basics of the focus trap function
Focus traps function on these basic principles.
- Define a collection of elements that can receive focus within the target parent element.
- Work out the first and last elements to determine when to loop forward and backward.
- Add listeners for forward [Tab] and backward [Shift+Tab] movement and dismissal [Esc].
- Apply the trap to the target element(s).
Our script below is pretty vanilla, and won’t handle some edge cases. (For example, we are using only standard key combos for navigation). But it does illustrate the core principles of focus traps and will be useful in most applications.
Key features used in this script include:
- Document Object Model (DOM) selection with querySelector and querySelectorAll
- Event handling with addEventListener
- Conditional checks using key codes and names for keyboard events like the Tab (9) and Escape (27) keys.
And here is the full script:
function trapFocus(element, controller)
{
var focusableEls = element.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])');
var firstFocusableEl = focusableEls[0];
var lastFocusableEl = focusableEls[focusableEls.length - 1];
var KEYCODE_TAB = 9;
var KEYCODE_ESC = 27;
element.addEventListener('keydown', function(e)
{
var isTabPressed = (e.key === 'Tab' || e.keyCode === KEYCODE_TAB);
var isEscPressed = (e.key === 'Escape' || e.keyCode === KEYCODE_ESC);
if (isEscPressed && firstFocusableEl.classList.contains("open"))
{
controller.click();
controller.focus();
}
else if (!isTabPressed)
{
return;
}
if ( e.shiftKey ) /* shift + tab */
{
if (document.activeElement === firstFocusableEl && firstFocusableEl.classList.contains("open"))
{
lastFocusableEl.focus();
e.preventDefault();
}
}
else /* tab */
{
if (document.activeElement === lastFocusableEl)
{
firstFocusableEl.focus();
e.preventDefault();
}
}
});
}
trapFocus(document.querySelector('#mobilesearch-wrapper'), document.querySelector('#mobilesearch'));
The contained focus area is defined as #mobilesearch-wrapper, and we handle the [Escape] key to close the search element.
We use the focusableEls variable to store all elements inside the focus trap that can receive focus. The firstFocusableEl and lastFocusableEl variables are critical for the tabbing order of the elements.
Note that e.preventDefault(); disables the default behavior of the [Tab] key, ensuring that your defined focus trap takes precedence.
Those are the basics. Again, you may come across some edge cases, but for most applications this vanilla JavaScript, with minor modification, will help you dramatically improve the accessibility of your web elements.
Focus traps in all kinds of action
I mentioned earlier that the vanilla function I discuss in this post won’t handle a lot of edge use cases. If you want to see how far focus traps can be stretched, I’d suggest this demo that illustrates numerous custom traps, including nested traps and ones that reveal additional focus elements once the trap is engaged by the user. It’s pretty neat stuff.
Focus on accessibility and good design
Focus traps are an important but often overlooked part of accessible website design. In this post, we’ve looked at a basic JavaScript function to implement this useful behavior and discussed a few additional considerations as you optimize your site for accessibility and usability.