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.