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

Restoring ActiveElement Focus After A User-Interaction In JavaScript

By Ben Nadel on

Yesterday, I looked at trapping focus within an element such that a user couldn't use keyboard-based navigation to tab outside of the given element. That kind of technique would be helpful in a modal window scenario where you don't want the active-focus to leave the modal. However, if the user closes the modal window, we would like to return focus to the previously-active element so that the user can pickup where they left-off in their workflow. We can use the document.activeElement reference to record and then subsequently restore focus via JavaScript.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

NOTE: A much more robust example of this technique can be seen in Inclusive Components by Heydon Pickering. This post is just me trying it out for myself.

The document.activeElement is a read-only property that contains the DOM (Document Object Model) element that currently has focus. We cannot assign a value to this property; however, if we call .focus() on another element within the DOM, we will implicitly cause said element to be assigned to the document.activeElement property.

In a modal window scenario, where we are drawing focus away from the "trigger" element and into the modal window context, we can use the document.activeElement property to record which element triggered the modal. And then, when the user subsequently closes the modal window, we can call .focus() on our recorded value to restore focus the previously-active element. This will make Tab-based navigation around the DOM much more fluid and accessible.

To see this in action, I've created a JavaScript demo with a bunch of button elements that all trigger a single modal window. And, when the user closes the modal window, I'm going to restore focus to the original button reference.

NOTE: For the sake of simplicity, I am not including the focus-trapping technique from yesterday's post. This demo is intended to isolate the use of the .activeElement property only.

<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8" />
	<title>
		Restoring ActiveElement Focus After A User-Interaction In JavaScript
	</title>
	<link rel="stylesheet" type="text/css" href="./demo.css" />
</head>
<body>

	<h1>
		Restoring ActiveElement Focus After A User-Interaction In JavaScript
	</h1>

	<p class="trigger">
		<button>Open modal (1)</button> , <button>Open modal (2)</button> ,
		<button>Open modal (3)</button> , <button>Open modal (4)</button>
	</p>
	<p class="trigger">
		<button>Open modal (5)</button> , <button>Open modal (6)</button> ,
		<button>Open modal (7)</button> , <button>Open modal (8)</button>
	</p>

	<!--
		Our modal window is going to be hidden by default. When triggered, it will take
		over the focus; and, when closed, focus will be returned to the element that
		originally triggered it.
	-->
	<div class="modal">
		<div role="dialog" aria-labelledby="modal-title" class="modal__panel">
			<h2 id="modal-title">
				Hello, Modal
			</h2>
			<p>
				<button>Close</button>
			</p>
		</div>
	</div>

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

		var trigger = $( ".trigger" )
			.on( "click", "button", openModal )
		;
		var modal = $( ".modal" )
			.on( "click", handleModalClick )
		;
		var panel = modal
			.find( ".modal__panel" )
			// We're applying tabindex to the modal panel so that we can programmatically
			// focus the panel after we open the modal window.
			.attr( "tabindex", "-1" )
			.on( "click", "button", closeModal )
		;

		// I keep track of element that triggered the modal window.
		var previousElement = null;

		// --------------------------------------------------------------------------- //
		// --------------------------------------------------------------------------- //

		// I open the modal window and draw focus into the modal container.
		function openModal() {

			// We're about to open the modal window and draw focus into the modal panel.
			// But, before we do that, we want to track which element triggered the modal
			// so that we can restore focus to that element when the modal is closed. 
			previousElement = ( document.activeElement || document.body );

			modal.addClass( "modal--open" );
			panel.focus();

			console.group( "Taking focus away from trigger" );
			console.log( previousElement );
			console.groupEnd();

		}


		// I close the modal window and return focus to the previous element.
		function closeModal() {

			modal.removeClass( "modal--open" );

			// If we have a reference to the original trigger, let's restore focus to
			// that the trigger so the user can pick-up where they left off.
			if ( previousElement ) {

				console.group( "Restoring focus to previously-active element" );
				console.log( previousElement );
				console.groupEnd();

				previousElement.focus();
				previousElement = null;

			}

		}


		// I handle top-level clicks on the modal.
		function handleModalClick( event ) {

			// If the user is clicking directly on the backdrop of the modal, let's
			// consider this a request to close the modal (a common interaction model).
			if ( modal.is( event.target ) ) {

				closeModal();

			}

		}

	</script>

</body>
</html>

As you can see, I have a global variable - previousElement - that stores the document.activeElement value at the time the modal window is opened. Then, when the user closes the modal window, I'm simply calling .focus() on this reference before unsetting it. This returns focus to the original trigger where the user can continue to Tab-navigate through the document.

Now, if we run this JavaScript demo in the browser and look at the console-logging, we can see the value of this previousElement variable as it consumed in the modal-window workflow:

Active element being maintained across a modal window workflow in JavaScript

As you can see, whenever the user opens and then closes the modal window, focus is returned to the trigger button. And, the user can continue to tab-through the buttons from whence they left-off.

In this demo, I'm exaggerating the :focus state of elements to make it super obvious where the user's focus is located. However, this kind of focus-restoring workflow really illustrates just how critical it is to have some sort of indication as to what has focus. I'm so embarrassed, in retrospect, at all the times I've set outline:none because having an outline didn't "fit with our design". So shameful.



Reader Comments

What has two thumbs and hopes you leave a comment? This Guy! (Ben Nadel).

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.