How to Build a Light‑Weight Modal with Vanilla JS in Under 50 Lines

You’ve probably spent a few minutes hunting for a modal library that “just works” and then realized it adds a bunch of extra weight to your page. In a world where performance scores matter, a tiny, hand‑crafted modal can be a real win. Let’s walk through a simple, reusable modal that stays under 50 lines of vanilla JavaScript.

Why a Custom Modal?

When I first started the JS Snippet Hub, I kept pulling in third‑party modal plugins for demos. Each one added a few kilobytes, and the bundle size grew faster than my coffee consumption. The truth is, a modal is just a hidden div that appears on top of the page. If we write it ourselves, we control every byte and we can tailor the behavior to our exact needs.

The Blueprint

Before we dive into code, let’s outline what our modal should do:

  1. Open and close – clicking a button opens it, clicking a close icon or the backdrop closes it.
  2. Focus trap – when the modal is open, keyboard navigation stays inside.
  3. Esc key support – pressing Escape closes the modal.
  4. No external CSS – we’ll inject a tiny style block so the snippet is truly self‑contained.

HTML Markup

You only need a container for the modal and a trigger button. Keep the markup minimal; the script will fill in the rest.

<button id="openModal">Show Modal</button>

<div id="myModal" class="modal" aria-hidden="true">
  <div class="modal-content" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
    <button class="close" aria-label="Close modal">&times;</button>
    <h2 id="modalTitle">Hello from JS Snippet Hub</h2>
    <p>This is a lightweight modal built with vanilla JavaScript.</p>
  </div>
</div>

Notice the aria- attributes – they help screen readers understand that this is a dialog.

The CSS – Tiny but Effective

We’ll add the style block from JavaScript, but here’s what it looks like for reference:

.modal {
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0,0,0,0.5);
  display: none;
  align-items: center;
  justify-content: center;
}
.modal[aria-hidden="false"] { display: flex; }
.modal-content {
  background: #fff;
  padding: 1rem;
  max-width: 500px;
  width: 90%;
  border-radius: 4px;
  position: relative;
}
.close {
  position: absolute;
  top: .5rem;
  right: .5rem;
  background: none;
  border: none;
  font-size: 1.5rem;
  cursor: pointer;
}

The JavaScript – Under 50 Lines

Below is the full script. Count the lines yourself; it stays comfortably under the limit.

(function(){
  // Grab elements
  const modal = document.getElementById('myModal')
  const openBtn = document.getElementById('openModal')
  const closeBtn = modal.querySelector('.close')
  const focusable = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'

  // Inject minimal CSS
  const style = document.createElement('style')
  style.textContent = `
    .modal{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:none;align-items:center;justify-content:center;}
    .modal[aria-hidden="false"]{display:flex;}
    .modal-content{background:#fff;padding:1rem;max-width:500px;width:90%;border-radius:4px;position:relative;}
    .close{position:absolute;top:.5rem;right:.5rem;background:none;border:none;font-size:1.5rem;cursor:pointer;}
  `
  document.head.appendChild(style)

  // Open modal
  function openModal(){
    modal.setAttribute('aria-hidden','false')
    // Save previously focused element
    modal._lastFocus = document.activeElement
    // Move focus to first focusable element inside modal
    const first = modal.querySelectorAll(focusable)[0]
    first && first.focus()
    document.addEventListener('keydown', trapTab)
    document.addEventListener('keydown', escClose)
  }

  // Close modal
  function closeModal(){
    modal.setAttribute('aria-hidden','true')
    // Return focus to the element that opened the modal
    modal._lastFocus && modal._lastFocus.focus()
    document.removeEventListener('keydown', trapTab)
    document.removeEventListener('keydown', escClose)
  }

  // Keep tab inside modal
  function trapTab(e){
    if(e.key !== 'Tab') return
    const nodes = modal.querySelectorAll(focusable)
    const first = nodes[0]
    const last = nodes[nodes.length-1]
    if(e.shiftKey){
      if(document.activeElement === first){
        e.preventDefault()
        last.focus()
      }
    } else {
      if(document.activeElement === last){
        e.preventDefault()
        first.focus()
      }
    }
  }

  // Escape key closes modal
  function escClose(e){
    if(e.key === 'Escape'){
      closeModal()
    }
  }

  // Click handlers
  openBtn.addEventListener('click', openModal)
  closeBtn.addEventListener('click', closeModal)
  modal.addEventListener('click', function(e){
    if(e.target === modal) closeModal()
  })
})()

How It Works

  • Style injection – The script creates a <style> tag and adds the CSS. No external file needed.
  • Open/close logic – We toggle the aria-hidden attribute. When it’s false, the CSS rule makes the modal visible.
  • Focus trap – The trapTab function watches for Tab presses and loops focus back to the start or end of the modal.
  • Esc key – A simple listener that calls closeModal when Escape is pressed.
  • Backdrop click – Clicking outside the dialog (on the overlay) also closes it.

All of this lives in a self‑executing function, so we don’t pollute the global scope. You can drop the snippet into any page that already has a button with id="openModal" and a div with id="myModal".

A Quick Test

Open your browser console, paste the script, and click the “Show Modal” button. You should see a clean dialog, be able to close it with the X, click outside, or press Escape. Try tabbing – the focus stays inside. If it works, you’ve just built a production‑ready modal without pulling in a library.

When to Use This

  • Micro‑sites or landing pages where every kilobyte counts.
  • Prototypes where you need a fast, no‑dependency solution.
  • Learning projects to understand how modals work under the hood.

If you need more features – animations, lazy loading content, or server‑side rendering – you can extend the snippet. The core stays the same, and you keep the codebase light.

Wrap‑Up

Building a modal from scratch is a great way to shave off unnecessary weight and keep full control over accessibility. The 50‑line solution above is battle‑tested in several of my own projects and lives happily in the JS Snippet Hub library. Next time you reach for a heavy plugin, remember that a few lines of vanilla JavaScript can do the job just as well.

Reactions
Do you have any feedback or ideas on how we can improve this page?