Skip to content

Instantly share code, notes, and snippets.

@guelzow
Last active June 12, 2025 13:56
Show Gist options
  • Save guelzow/a580d23bd2079dccd4d85647a18cffb9 to your computer and use it in GitHub Desktop.
Save guelzow/a580d23bd2079dccd4d85647a18cffb9 to your computer and use it in GitHub Desktop.
Focus Trap
/**
* Focus Trap
*
* This script provides a focus trap functionality for modal dialogs.
*
* @usage
* To use this focus trap, call `focusTrap.init()` with the selector of the modal dialog element.
* You can also provide an optional selector for a close button within the modal dialog.
* This will ensure that when the modal dialog is open, the focus remains within the dialog,
* cycling through the focusable elements and preventing focus from leaving the dialog.
*
* Example 01:
* focusTrap.init('#myModal', '.close-button');
* This will initialize the focus trap for the modal dialog with the ID `myModal`
* and the close button with the class `close-button`.
*
* Example 02:
* focusTrap.init('#myModal');
* Init the focus trap for the modal dialog with the ID `myModal`
* without a close button.
* gallery.listen('destroy', focusTrap.destroy);
* This will destroy the focus trap when the modal dialog is closed.
* Keep in mind, that there are a lot of different modal dialog implementations,
* so you might need to adapt the event registration.
* This example is for PhotoSwipe.
*
* Example 03:
* focusTrap.init('#headerSearch');
* This will initialize the focus trap for the modal dialog with the ID `headerSearch`.
* focusTrap.setEscapeKeyListener(function (event) {
* $headersearch.collapse('hide');
* });
* This will set a custom listener for the Escape key,
* which will collapse the header search when the Escape key is pressed.
*
* Example 04:
* focusTrap.updateFocusableElements();
* This will update the list of focusable elements within the modal dialog.
* Normally, you do not need to call this function, but it can be useful if the modal dialog content changes
* dynamically or if new focusable elements are added or removed.
*
*/
(function () {
'use strict';
const focusTrap = {};
focusTrap.modalElement = null;
focusTrap.focusableElements = null;
focusTrap.firstElement = null;
focusTrap.lastElement = null;
focusTrap.closeButton = null;
focusTrap.escapeKeyListener = function () {};
focusTrap.element = null;
/**
* Initializes the focus trap for a modal dialog.
*
* This function sets up the focus trap by
* finding all focusable elements within the modal dialog
* and adding an event listener for the keydown event.
* It ensures that when the Tab key is pressed,
* the focus remains within the modal dialog,
* cycling through the focusable elements.
*
* @param {String} elementSelector
* The selector for the modal dialog element.
* This should be a valid CSS selector
* that matches the modal dialog you want to trap focus within.
* @param {String} closeButtonSelector
* Optional selector for a close button within the modal dialog.
* If provided, clicking this button will destroy the focus trap.
* If you do not provide a close button selector,
* you will have to call `focusTrap.destroy()` manually.
*
* @returns {void}
*/
focusTrap.init = function (elementSelector, closeButtonSelector) {
focusTrap.modalElement = document.querySelector(elementSelector);
focusTrap.modalElement.addEventListener('keydown', focusTrap.handleTabKeyPress);
focusTrap.updateFocusableElements();
if (closeButtonSelector) {
focusTrap.closeButton = focusTrap.modalElement.querySelector(closeButtonSelector);
if (focusTrap.closeButton) {
focusTrap.closeButton.addEventListener('click', focusTrap.destroy);
focusTrap.closeButton.focus();
}
}
if (focusTrap.closeButton) {
focusTrap.closeButton.focus();
} else {
focusTrap.firstElement.focus();
}
}
/**
* Updates the list of focusable elements within the modal dialog.
*
* This function scans the modal dialog for all focusable elements and updates the `focusableElements`,
* `firstElement`, and `lastElement` properties.
* It should be called whenever the modal dialog content changes or when the focusable elements need to be refreshed.
* This is useful if the modal dialog content is dynamic or if new focusable elements are added or removed.
*
* @returns {void}
*/
focusTrap.updateFocusableElements = function () {
focusTrap.focusableElements = [];
let el, parentEl, isFocusable;
let possibleElements = focusTrap.modalElement.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
for (let i = 0; i < possibleElements.length; i++) {
el = possibleElements[i];
parentEl = el;
isFocusable = true;
while (parentEl) {
// If the element is inside a hidden parent, skip it
if (
window.getComputedStyle(parentEl, null).display === 'none'
|| parentEl.style.display === 'none'
|| parentEl.disabled
|| window.getComputedStyle(parentEl, null).visibility === 'hidden'
|| parentEl.style.visibility === 'hidden'
) {
isFocusable = false;
break;
}
parentEl = parentEl.parentElement;
}
// Check if the element is visible and not disabled
if (isFocusable) {
focusTrap.focusableElements.push(el);
}
}
focusTrap.firstElement = focusTrap.focusableElements[0];
focusTrap.lastElement = focusTrap.focusableElements[focusTrap.focusableElements.length - 1];
}
/**
* Destroys the focus trap by removing the event listener and resetting the focus trap properties.
*
* This function should be called when the modal dialog is closed
* to clean up the focus trap state.
*/
focusTrap.destroy = function () {
if (focusTrap.modalElement) {
focusTrap.modalElement.removeEventListener('keydown', focusTrap.handleTabKeyPress);
}
if (focusTrap.closeButton) {
focusTrap.closeButton.removeEventListener('click', focusTrap.destroy);
}
focusTrap.closeButton = null;
focusTrap.modalElement = null;
focusTrap.focusableElements = null;
focusTrap.firstElement = null;
focusTrap.lastElement = null;
focusTrap.element = null;
focusTrap.escapeKeyListener = function () {};
}
/**
* Handles the Tab key press to trap focus within the modal dialog.
*
* This function prevents the focus from leaving the modal dialog
* and cycles through the focusable elements.
*
* @param {KeyboardEvent} event - The keydown event triggered by pressing the Tab key.
*
* @returns {void}
*/
focusTrap.handleTabKeyPress = function (event) {
focusTrap.updateFocusableElements();
if (event.key === 'Tab') {
let activeElem = document.activeElement;
if (event.shiftKey && activeElem.isEqualNode(focusTrap.firstElement)) {
event.preventDefault();
focusTrap.lastElement.focus();
return;
}
if (!event.shiftKey && activeElem.isEqualNode(focusTrap.lastElement)) {
event.preventDefault();
focusTrap.firstElement.focus();
}
}
if (event.key === 'Escape') {
event.preventDefault();
focusTrap.escapeKeyListener(event);
}
}
/**
* Sets a custom listener for the Escape key.
*
* This function allows you to define a custom action
* when the Escape key is pressed while the focus trap is active.
* You can use this to close the modal dialog or perform any other action.
*
* @param {Function} callback
* The callback function to be executed when the Escape key is pressed.
* Takes the {KeyboardEvent} event as a parameter.
*
* @returns {void}
*/
focusTrap.setEscapeKeyListener = function (callback) {
if (typeof callback !== 'function') {
throw new Error('focusTrap.setEscapeKeyListener expects a function as an argument.');
}
focusTrap.escapeKeyListener = callback;
};
// Export the focusTrap object
window.focusTrap = focusTrap;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment