Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Trapping Focus Within An Element Using Tab-Key Navigation In JavaScript

By Ben Nadel on

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 Tab or Shift+Tab to navigate, they never navigate to an element that is outside of the modal.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

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: Adding 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 Tab and 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 href attributes).
  • Anything with tabindex where the value is not -1.

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:

<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8" />
	<title>
		Trapping Focus Within An Element Using Tab-Key Navigation In JavaScript
	</title>

	<link rel="stylesheet" type="text/css" href="./demo.css" />
</head>
<body>

	<h1>
		Trapping Focus Within An Element Using Tab-Key Navigation In JavaScript
	</h1>

	<p>
		<a href="#">I can be focused</a> ,
		<a href="#">I can be focused</a> ,
		<a href="#">I can be focused</a> ,
		<a href="#">I can be focused</a>
	</p>

	<!-- Once focus lands inside this container, we're going to trap it there. -->
	<p class="capture">
		<input type="text" />
		<a tabindex="-1">I am <strong>NOT</strong> tabbable</a>
		<a href="#">I can be focused</a>
		<a href="#">I can be focused</a>
		<button>Hey buddy</button>
		<a href="#">I can be focused</a>
		<input type="checkbox" />
		<a href="#">I can be focused</a>
		<a tabindex="-1">I am <strong>NOT</strong> tabbable</a>
		<button>Hey buddy</button>
	</p>

	<p>
		<a href="#">I can be focused</a> ,
		<a href="#">I can be focused</a> ,
		<a href="#">I can be focused</a> ,
		<a href="#">I can be focused</a>
	</p>

	<script type="text/javascript" src="../../vendor/jquery/3.6.0/jquery-3.6.0.min.js"></script>
	<script type="text/javascript">

		var capture = $( ".capture" )
			// For the sake of the demo, when the page loads we're going to draw focus to
			// the capture container. By using a tabindex of -1, the capture container
			// won't be focusable via keyboard navigation; but, we can programmatically
			// focus the element.
			.attr( "tabindex", "-1" )
			.focus()
			// Inpsect any keydown events that come from within the capture container.
			.keydown(
				function handleKeydown( event ) {

					if ( event.key.toLowerCase() !== "tab" ) {

						return;

					}

					// At this point, we know that we're capturing a TAB-related keyboard
					// event ON or IN the Capture container. As such, we need to look at
					// the current state of the DOM (which may be changing dynamically
					// depending on the application logic) to see if we need to override
					// the keyboard navigation. What we're looking for here is any
					// element that is capable of NATURALLY receiving focus (via Tab).
					// --
					// NOTE: jQuery's .add() function results in a collection that is
					// ordered by the DOM ORDER. As such, we can use .first() and .last()
					// on the resultant collection with confidence.
					var tabbable = $()
						// All form elements can receive focus.
						.add( capture.find( "button, input, select, textarea" ) )
						// Any element that has an HREF can receive focus.
						.add( capture.find( "[href]" ) )
						// Any element that has a non-(-1) tabindex can receive focus.
						.add( capture.find( "[tabindex]:not([tabindex='-1'])" ) )
					;
					var target = $( event.target );

					// Reverse tabbing (Key: Shift+Tab).
					if ( event.shiftKey ) {

						if ( target.is( capture ) || target.is( tabbable.first() ) ) {

							// Force focus to last element in container.
							event.preventDefault();
							tabbable.last().focus();

						}

					// Forward tabbing (Key: Tab).
					} else {

						if ( target.is( tabbable.last() ) ) {

							// Force focus to first element in container.
							event.preventDefault();
							tabbable.first().focus();

						}

					}

				}
			)
		;

	</script>

</body>
</html>

As you can see, when the demo loads, I'm programmatically applying a tabindex="-1" to the container element, which allows me to focus the container using JavaScript. This is something we'd want to do when opening a modal window - drawing focus into the modal context immediately.

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 shiftKey.

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.

And, when we run this JavaScript demo and attempt to navigate beyond the container elements, we get the following behavior:

Keyboard-based navigation being trapped within a container element using JavaScript.

As you can see, once focus enters the container element, using Tab or Shift+Tab navigation keeps the focus within the container element's tabbable boundaries.

Managing focus within an accessible application is more complex than trapping keyboard events. For example, you should return focus to the previously active element if the user closes the container element. But, that's beyond my skills and fodder for a future post. The first step is just to figure out how to keep focus within a container element using JavaScript.



Reader Comments

@All,

As a follow-up post, I wanted to look at how to restore focus to an element. As I mentioned in this post, one of the key use-cases for trapping keyboard navigation is for modal windows. Another common problem with modal windows is taking-over focus and then returning focus after the modal window is closed:

www.bennadel.com/blog/4097-restoring-activeelement-focus-after-a-user-interaction-in-javascript.htm

We can keep track of the focus element with the document.activeElement property.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.