Restoring ActiveElement Focus After A User-Interaction In JavaScript
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:
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
Hey man, Junior Front End Developer here. This post about restoring focus after user interaction really helped me out, thank you!
@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!
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).
@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.