Modal Project
title: "Modal Project — Build a Complete Accessible Modal System" description: "Build a production-ready modal component system combining focus trapping, inert backdrop, Escape handling, and screen reader support." weight: 12 date: 2026-06-28 lastmod: 2026-06-28 tags: [accessibility, modals]
Apply everything you learned by building a complete accessible modal system that handles focus, inert background, close methods, and screen reader announcements.
## What You'll Learn
How to combine all modal patterns into a reusable component that works for keyboard, screen reader, and mobile users.
## Why It Matters
A reusable modal component ensures consistency across all dialogs in your application, reducing the risk of introducing accessibility issues.
## Real-World Use
Doda Browser uses a modal system for settings, confirmations, and file operations. Every modal follows the same pattern: focus trap, inert background, Escape to close, and proper ARIA attributes.
## Complete Modal
```html
<div id="page-container">
<header role="banner">...</header>
<main>...</main>
<footer role="contentinfo">...</footer>
</div>
<div id="modal-overlay" role="dialog" aria-modal="true"
aria-labelledby="modal-title" aria-describedby="modal-desc"
hidden>
<div class="modal-content">
<h2 id="modal-title" tabindex="-1">Modal title</h2>
<p id="modal-desc">Modal description text.</p>
<button onclick="closeModal()" aria-label="Close"
class="close-btn">X</button>
<div class="modal-body">
<!-- Dynamic content -->
</div>
<button class="primary" onclick="confirm()">Confirm</button>
<button onclick="closeModal()">Cancel</button>
</div>
</div>
JavaScript
const pageContainer = document.getElementById('page-container');
const modal = document.getElementById('modal-overlay');
let previousFocus = null;
let scrollPosition = 0;
function openModal(title, description, contentHtml) {
previousFocus = document.activeElement;
scrollPosition = window.scrollY;
document.getElementById('modal-title').textContent = title;
document.getElementById('modal-desc').textContent = description;
document.querySelector('.modal-body').innerHTML = contentHtml;
modal.hidden = false;
pageContainer.inert = true;
document.body.style.overflow = 'hidden';
modal.querySelector('h2').focus();
modal.addEventListener('keydown', handleKeyDown);
modal.addEventListener('click', handleBackdrop);
}
function closeModal() {
modal.hidden = true;
pageContainer.inert = false;
document.body.style.overflow = '';
modal.removeEventListener('keydown', handleKeyDown);
modal.removeEventListener('click', handleBackdrop);
window.scrollTo(0, scrollPosition);
if (previousFocus) previousFocus.focus();
}
function handleKeyDown(e) {
if (e.key === 'Escape') {
closeModal();
return;
}
if (e.key === 'Tab') {
const focusable = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
}
function handleBackdrop(e) {
if (e.target === modal) closeModal();
}
Common Mistakes
1. Not cleaning up event listeners
Remove all event listeners when the modal closes to prevent memory leaks and duplicate handlers.
2. No loading state for async content
If modal content loads asynchronously, show a loading indicator with role=status.
3. Focus trap not recalculated for dynamic content
If modal content changes after opening, re-query focusable elements for the focus trap.
4. Scroll position not restored on close
Save scrollY before opening and restore it on close to prevent position jumps.
5. No error state
If modal content fails to load, show an error message with role=alert and focus management.
Practice Questions
1. What properties should the modal manager save on open? Previous focus element, scroll position, and any modal-specific state.
2. Why should event listeners be removed on close? To prevent memory leaks and ensure fresh event handlers when the modal reopens.
3. What happens if the trigger element is missing on close? Provide a fallback: focus the page title, the main element, or the first focusable element.
Challenge: Extend the modal system to support stacked modals (modal opening another modal). Each modal must save and restore its own focus.
FAQ
Mini Project
Build the complete modal system and create three modal instances: a confirmation dialog, a form dialog, and an information dialog. Test all interactions.
What's Next
Now explore Accessible Data Tables to learn how to build tables that screen reader users can navigate efficiently.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro