Skip to content

Instantly share code, notes, and snippets.

Last active December 1, 2023 14:39
Show Gist options
  • Save samthor/babe9fad4a65625b301ba482dad284d1 to your computer and use it in GitHub Desktop.
Save samthor/babe9fad4a65625b301ba482dad284d1 to your computer and use it in GitHub Desktop.
Restore focus after a HTML dialog is shown modally
* Updates the passed dialog to retain focus and restore it when the dialog is closed. Won't
* upgrade a dialog more than once. Supports IE11+ and is a no-op otherwise.
* @param {!HTMLDialogElement} dialog to upgrade
var registerFocusRestoreDialog = (function() {
if (!window.WeakMap || !window.MutationObserver) {
return function() {};
var registered = new WeakMap();
// store previous focused node centrally
var previousFocus = null;
document.addEventListener('focusout', function(ev) {
previousFocus =;
}, true);
return function registerFocusRestoreDialog(dialog) {
if (dialog.localName !== 'dialog') {
throw new Error('Failed to upgrade focus on dialog: The element is not a dialog.');
if (registered.has(dialog)) { return; }
registered.set(dialog, null);
// replace showModal method directly, to save focus
var realShowModal = dialog.showModal;
dialog.showModal = function() {
var savedFocus = document.activeElement;
if (savedFocus === document || savedFocus === document.body) {
// some browsers read activeElement as body
savedFocus = previousFocus;
registered.set(dialog, savedFocus);;
// watch for 'open' change and clear saved
var mo = new MutationObserver(function() {
if (!dialog.hasAttribute('open')) {
registered.set(dialog, null);
} else {
// if open was cleared/set in the same frame, then the dialog will still be a modal (Y)
mo.observe(dialog, {attributes: true, attributeFilter: ['open']});
// on close, try to focus saved, if possible
dialog.addEventListener('close', function(ev) {
if (dialog.hasAttribute('open')) {
return; // in native, this fires the frame later
var savedFocus = registered.get(dialog);
if (document.contains(savedFocus)) {
var wasFocus = document.activeElement;
if (document.activeElement !== savedFocus) {
wasFocus.focus(); // restore focus, we couldn't focus saved
savedFocus = null;
// FIXME: If a modal dialog is readded to the page (either remove/add or .appendChild), it will
// be a non-modal. It will still have its 'close' handler called and try to focus on the saved
// element.
// These could basically be solved if 'close' yielded whether it was a modal or non-modal
// being closed. But it doesn't. It could also be solved by a permanent MutationObserver, as is
// done inside the polyfill.
Copy link

sntran commented Feb 9, 2019

When testing this on Chrome version 71.0.3578.98 (Official Build) (64-bit), the focus is not transferred back to the saved element.

It looks like the MutationObserver clears the saved element before the close handler runs.

Here is an example:

Copy link

Leland commented Dec 1, 2023

Why localName and not nodeName?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment