JAVASCRIPT
Build a Reusable and Accessible JavaScript Modal Dialog
Discover how to programmatically create and manage an accessible modal dialog, including focus trapping, keyboard navigation (Esc key), and dynamic content injection.
class AccessibleModal {
constructor(options = {}) {
this.options = {
title: 'Modal Title',
content: 'Modal Content Here',
onClose: () => {},
...options
};
this.modal = null;
this.focusableElements = null;
this.firstFocusableElement = null;
this.lastFocusableElement = null;
this.previouslyFocusedElement = null;
this._createModal();
this._bindEvents();
}
_createModal() {
this.modal = document.createElement('div');
this.modal.setAttribute('role', 'dialog');
this.modal.setAttribute('aria-modal', 'true');
this.modal.setAttribute('aria-labelledby', 'modal-title');
this.modal.classList.add('js-accessible-modal');
this.modal.innerHTML = `
<div class="js-modal-overlay" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); display: flex; justify-content: center; align-items: center; z-index: 1000;">
<div class="js-modal-content" style="background: white; padding: 20px; border-radius: 5px; max-width: 500px; width: 90%; box-shadow: 0 4px 8px rgba(0,0,0,0.2);">
<h2 id="modal-title" style="margin-top: 0;">${this.options.title}</h2>
<div class="modal-body">${this.options.content}</div>
<button class="js-modal-close" style="float: right; margin-top: 10px; padding: 8px 15px; background-color: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer;">Close</button>
</div>
</div>
`;
document.body.appendChild(this.modal);
}
_bindEvents() {
this.closeButton = this.modal.querySelector('.js-modal-close');
this.overlay = this.modal.querySelector('.js-modal-overlay');
this.closeButton.addEventListener('click', this.close.bind(this));
this.overlay.addEventListener('click', (event) => {
if (event.target === this.overlay) {
this.close();
}
});
document.addEventListener('keydown', this._handleKeydown.bind(this));
}
_handleKeydown(event) {
if (event.key === 'Escape') {
this.close();
} else if (event.key === 'Tab') {
this._handleTabKey(event);
}
}
_handleTabKey(event) {
if (!this.focusableElements) {
this._setupFocusTrap(); // Ensure focusable elements are identified
}
if (!this.focusableElements.length) {
event.preventDefault(); // If no focusable elements, prevent tab from leaving modal
return;
}
const focusedElement = document.activeElement;
const isFirstFocusable = focusedElement === this.firstFocusableElement;
const isLastFocusable = focusedElement === this.lastFocusableElement;
if (event.shiftKey) { // Shift + Tab
if (isFirstFocusable) {
this.lastFocusableElement.focus();
event.preventDefault();
}
} else { // Tab
if (isLastFocusable) {
this.firstFocusableElement.focus();
event.preventDefault();
}
}
}
_setupFocusTrap() {
this.focusableElements = Array.from(
this.modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
).filter(el => !el.disabled && el.offsetParent !== null); // Filter out disabled/hidden elements
this.firstFocusableElement = this.focusableElements[0];
this.lastFocusableElement = this.focusableElements[this.focusableElements.length - 1];
}
open() {
this.previouslyFocusedElement = document.activeElement; // Save focus
document.body.style.overflow = 'hidden'; // Prevent scrolling body
this.modal.style.display = 'block';
setTimeout(() => { // Delay to ensure modal is rendered before attempting focus
this._setupFocusTrap();
if (this.firstFocusableElement) {
this.firstFocusableElement.focus();
} else {
this.closeButton.focus(); // Fallback to close button
}
}, 0);
}
close() {
this.modal.remove();
document.body.style.overflow = ''; // Restore body scrolling
document.removeEventListener('keydown', this._handleKeydown.bind(this));
if (this.previouslyFocusedElement) {
this.previouslyFocusedElement.focus(); // Restore focus
}
this.options.onClose();
}
}
// Usage example:
// const myModal = new AccessibleModal({
// title: 'Welcome!',
// content: '<p>This is a custom, accessible modal dialog built with plain JavaScript.</p>',
// onClose: () => console.log('Modal closed!')
// });
// setTimeout(() => myModal.open(), 1000); // Open after 1 second
How it works: This class provides a robust and accessible modal dialog solution. It dynamically creates all necessary modal elements, appends them to the DOM, and manages crucial accessibility features like focus trapping within the modal, keyboard navigation (Tab and Shift+Tab), and closing via the Escape key or clicking the overlay. It also prevents body scrolling while the modal is open and restores focus to the element that triggered the modal upon closing.