Building a Modal Dialog from Scratch
Modal dialogs are one of the most common UI patterns on the web, yet they're surprisingly easy to get wrong. In this guide, you'll build a fully functional, accessible modal from the ground up using only HTML, CSS, and vanilla JavaScript — no external libraries needed.
Step 1: The HTML Structure
A modal needs a backdrop overlay and a dialog container. Here's the essential markup:
<!-- Trigger Button -->
<button id="open-modal">Open Modal</button>
<!-- Modal Overlay -->
<div id="modal-overlay" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="modal-title" hidden>
<div class="modal-box">
<h2 id="modal-title">Modal Title</h2>
<p>This is the modal content area.</p>
<button id="close-modal">Close</button>
</div>
</div>
Key points in this structure:
- role="dialog" tells assistive technologies this is a dialog element.
- aria-modal="true" signals that content behind the modal is inert.
- aria-labelledby links the dialog to its visible title for screen readers.
- The hidden attribute hides the modal by default without relying purely on CSS.
Step 2: The CSS
The overlay should cover the full viewport and center the dialog box:
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-box {
background: #fff;
border-radius: 8px;
padding: 2rem;
max-width: 500px;
width: 90%;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
Use position: fixed so the modal stays in place even when the page is scrolled. The inset: 0 shorthand is equivalent to setting top, right, bottom, and left all to zero.
Step 3: The JavaScript Logic
The open/close logic is straightforward, but you need to handle a few edge cases:
const overlay = document.getElementById('modal-overlay');
const openBtn = document.getElementById('open-modal');
const closeBtn = document.getElementById('close-modal');
function openModal() {
overlay.removeAttribute('hidden');
closeBtn.focus(); // move focus into the modal
}
function closeModal() {
overlay.setAttribute('hidden', '');
openBtn.focus(); // return focus to the trigger
}
openBtn.addEventListener('click', openModal);
closeBtn.addEventListener('click', closeModal);
// Close on backdrop click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) closeModal();
});
// Close on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !overlay.hasAttribute('hidden')) closeModal();
});
Step 4: Prevent Background Scrolling
When a modal is open, users shouldn't be able to scroll the page behind it. Add this to your open and close functions:
// In openModal:
document.body.style.overflow = 'hidden';
// In closeModal:
document.body.style.overflow = '';
Summary
With roughly 50 lines of code, you have a functional modal that handles:
- Open and close via button and backdrop click
- Keyboard dismissal with the Escape key
- Focus management on open and close
- Background scroll prevention
- Proper ARIA semantics
From this foundation, you can extend the modal with animations, form content, confirmation dialogs, and more. The key is getting the basics right first.