Skip to main content
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Francois Anselmo and Eric Masciotra and Yann Ady and Stéphane Leclerc
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Francois Anselmo Eric Masciotra Yann Ady Stéphane Leclerc

Restoring ActiveElement Focus After A User-Interaction In JavaScript

By
Published in Comments (4)

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.

Want to use code from this post? Check out the license.

Reader Comments

15,902 Comments

@Brandon,

Awesome! Glad to hear it. Dealing with modal windows is a particularly tricky situation. I'm still trying to figure out best practices in terms of accessibility. Good luck to you on your journey! Web development is awesome fun!

1 Comments

Just a heads up Safari doesn't always store the clicked element in document.activeElement (just doing a quick test using a link for the click event and it's returning the body).

15,902 Comments

@Aj,

Good to know. Safari can definitely make things harder. Though, it seems recently that Safari is release more modern updates to their CSS. But yeah, they can be a bummer sometimes.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel