html|css|javascript 46 views

Custom Modal Dialog Component with Vanilla JavaScript

Create a reusable modal dialog with open/close functionality using only HTML, CSS, and JavaScript.

By TWC Team • Jan 25, 2026

Code

<!--
  Custom, reusable modal dialog (vanilla JS) with a11y: focus trap, ESC close, backdrop click.
  Usage: add data-modal-open="modalId" to any trigger; include a [data-modal-close] button inside.
-->
<button type="button" data-modal-open="demo-modal">Open Modal</button>

<div id="demo-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-hidden="true">
  <div class="modal__content" role="document" tabindex="-1">
    <button class="modal__close" type="button" aria-label="Close dialog" data-modal-close>&times;</button>
    <h2 id="modal-title">Modal Title</h2>
    <p>This is a modal dialog.</p>
    <button type="button" data-modal-close>Close</button>
  </div>
</div>

<style>
  .modal{display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.45);padding:clamp(16px,4vw,32px);overflow:auto}
  .modal[aria-hidden="false"]{display:block}
  .modal__content{background:#fff;margin:10vh auto;max-width:600px;border-radius:12px;padding:20px;border:1px solid rgba(0,0,0,.12);box-shadow:0 20px 60px rgba(0,0,0,.25);outline:0}
  .modal__close{appearance:none;border:0;background:transparent;float:right;font-size:28px;line-height:1;color:#666;cursor:pointer}
  .modal__close:hover,.modal__close:focus-visible{color:#000}
  @media (prefers-reduced-motion:no-preference){.modal__content{animation:pop .14s ease-out}}
  @keyframes pop{from{transform:translateY(6px);opacity:.7}to{transform:translateY(0);opacity:1}}
</style>

<script>
  // Minimal modal controller: supports multiple modals; safe focus handling; cleans up listeners on close.
  (() => {
    const selectors = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
    let activeModal = null, lastActiveEl = null, removeTrap = null;

    const getFocusable = (root) => [...root.querySelectorAll(selectors)].filter(el => el.offsetParent !== null);

    function openModal(modal) {
      if (!modal || modal === activeModal) return;
      if (activeModal) closeModal(activeModal);

      activeModal = modal;
      lastActiveEl = document.activeElement;

      modal.setAttribute('aria-hidden', 'false');
      document.body.style.overflow = 'hidden'; // Prevent background scroll (performance + UX)
      const panel = modal.querySelector('.modal__content') || modal;

      const focusables = getFocusable(modal);
      (focusables[0] || panel).focus({ preventScroll: true });

      const onKeydown = (e) => {
        if (e.key === 'Escape') return closeModal(modal);
        if (e.key !== 'Tab') return;

        const els = getFocusable(modal);
        if (!els.length) return e.preventDefault();

        const first = els[0], last = els[els.length - 1];
        if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
        else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
      };

      const onClick = (e) => {
        if (e.target === modal || e.target.closest('[data-modal-close]')) closeModal(modal);
      };

      modal.addEventListener('keydown', onKeydown);
      modal.addEventListener('click', onClick);

      removeTrap = () => {
        modal.removeEventListener('keydown', onKeydown);
        modal.removeEventListener('click', onClick);
      };
    }

    function closeModal(modal) {
      if (!modal) return;
      modal.setAttribute('aria-hidden', 'true');
      document.body.style.overflow = '';
      removeTrap?.(); removeTrap = null;
      activeModal = null;
      (lastActiveEl && lastActiveEl.focus) && lastActiveEl.focus({ preventScroll: true });
      lastActiveEl = null;
    }

    document.addEventListener('click', (e) => {
      const trigger = e.target.closest('[data-modal-open]');
      if (!trigger) return;
      const modalId = trigger.getAttribute('data-modal-open');
      openModal(document.getElementById(modalId));
    });

    // Example usage: <button data-modal-open="demo-modal">Open</button>
  })();
</script>
Back to Snippets