Skip to content

Modal Project

DodaTech 3 min read

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

Should I use a CSS class or JavaScript for modal animations?

Use CSS transitions for entry animations. JavaScript can add/remove classes to trigger them.

How do I handle modal content that is very long?

Set max-height on modal-content with overflow-y: auto. Ensure the focus trap recalculates for scrollable content.

Should the modal system support multiple instances?

Yes. Use a modal manager class that supports stacking and individual focus management.

How do I test the complete modal system?

Test keyboard: Tab cycle, Escape, Enter. Test screen reader: role announcement, name, description. Test mobile: touch close, scroll content.

Does the modal work with the browser back button?

If the modal is important, use the History API to handle back button dismissal.

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