How to Create a Modal Popup in CSS

This recipe builds an accessible modal dialog that opens on a button click and can be closed with a close button, the Escape key, or a click on the overlay.

What a modal needs to do

  1. Sit above every other element, dimming the page behind it with an overlay.
  2. Be clearly announced to assistive technology as a dialog, not just a styled div.
  3. Offer more than one way to close - a visible button at minimum, plus Escape.
  4. Stop the page underneath from scrolling while it is open.

Building the overlay and box

A fixed-position overlay covers the viewport and is flex-centered so the dialog box sits in the middle regardless of its own size. The overlay is hidden by default and shown by toggling a class; the dialog box itself carries role='dialog', aria-modal='true', and aria-labelledby pointing at its heading's id so screen readers identify it correctly.

Example

<!DOCTYPE html>
<html>
<head>
<style>
  body { font-family: sans-serif; margin: 40px; }
  .btn { padding: 10px 16px; background: #2563eb; color: white; border: none; border-radius: 4px; cursor: pointer; }

  .modal-overlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.5);
    justify-content: center;
    align-items: center;
    z-index: 100;
  }
  .modal-overlay.open { display: flex; }

  .modal-box {
    background: white;
    border-radius: 8px;
    padding: 24px;
    width: min(400px, 90vw);
    position: relative;
  }
  .modal-close {
    position: absolute;
    top: 12px;
    right: 12px;
    background: none;
    border: none;
    font-size: 1.2rem;
    cursor: pointer;
  }
</style>
</head>
<body>
  <button class='btn' id='openBtn'>Open modal</button>

  <div class='modal-overlay' id='overlay'>
    <div class='modal-box' role='dialog' aria-modal='true' aria-labelledby='modalTitle'>
      <button class='modal-close' id='closeBtn' aria-label='Close'>&times;</button>
      <h2 id='modalTitle'>Confirm action</h2>
      <p>Are you sure you want to continue? This is a fully working, self-contained modal.</p>
      <button class='btn' id='closeBtn2'>Got it</button>
    </div>
  </div>

  <script>
    const openBtn = document.getElementById('openBtn');
    const overlay = document.getElementById('overlay');
    const closeBtn = document.getElementById('closeBtn');
    const closeBtn2 = document.getElementById('closeBtn2');

    function openModal() {
      overlay.classList.add('open');
      document.body.style.overflow = 'hidden';
      closeBtn.focus();
    }

    function closeModal() {
      overlay.classList.remove('open');
      document.body.style.overflow = '';
      openBtn.focus();
    }

    openBtn.addEventListener('click', openModal);
    closeBtn.addEventListener('click', closeModal);
    closeBtn2.addEventListener('click', closeModal);

    overlay.addEventListener('click', (e) => {
      if (e.target === overlay) closeModal();
    });

    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape' && overlay.classList.contains('open')) closeModal();
    });
  </script>
</body>
</html>
Note: Move focus into the dialog when it opens (for example onto the close button) and back to the button that opened it when it closes - keyboard users otherwise lose track of where they are on the page.

Closing correctly, not just on outside click

Relying only on an outside click to dismiss a modal quietly breaks it for keyboard-only and touch users, who may have no equivalent gesture. This recipe wires up three independent ways out at once: an explicit close button, a click that lands on the dimmed backdrop itself (checked by comparing e.target to the overlay), and a global Escape key listener that works regardless of where focus currently is.

Example

<!DOCTYPE html>
<html>
<head>
<style>
  .modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); }
  .modal-overlay.open { display: block; }
  .modal-box { background: white; margin: 80px auto; padding: 20px; width: 300px; }
</style>
</head>
<body>
  <div class='modal-overlay' id='overlay'>
    <div class='modal-box'>
      <button id='closeBtn'>Close</button>
    </div>
  </div>

  <script>
    const overlay = document.getElementById('overlay');
    const closeBtn = document.getElementById('closeBtn');

    function closeModal() {
      overlay.classList.remove('open');
    }

    closeBtn.addEventListener('click', closeModal);

    overlay.addEventListener('click', (e) => {
      if (e.target === overlay) closeModal();
    });

    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape') closeModal();
    });
  </script>
</body>
</html>

Blocking background scroll

Setting document.body.style.overflow to 'hidden' while the modal is open, and clearing it back to an empty string when it closes, stops the page behind the dialog from scrolling - without this, a long page can scroll underneath the fixed overlay in a way that feels broken, and touch users can accidentally scroll the wrong layer entirely.

RequirementWhy it mattersHow this recipe handles it
Visible close controlUsers should not have to guess a gesture closes the modalAn X button in the corner, always visible
Escape key closes itKeyboard-only and screen-reader users may not click outside anythingA document-level keydown listener checks for Escape
Background does not scrollA scrollable page behind an open modal is disorientingbody gets overflow: hidden while the modal is open, cleared on close
Dialog is announced correctlyScreen readers need to know this is a modal dialog, not part of the pagerole='dialog', aria-modal='true', aria-labelledby pointing at the title
Focus lands somewhere sensibleKeyboard users need to land inside the dialog, then return to the triggerFocus moves to the close button on open, back to the opener button on close

Frequently Asked Questions

How do I create a popup modal in HTML and CSS?
Use a full-screen overlay div with a semi-transparent background and a centred content box inside it. Toggle a class to show and hide it. CSS handles the appearance; a few lines of JavaScript handle opening and closing.
How do I close a modal when clicking outside it?
Listen for clicks on the overlay and close only when the click target is the overlay itself, not a child. Checking event.target === overlay stops clicks inside the modal from bubbling up and closing it unexpectedly.
Can I make a modal without JavaScript?
Yes. The HTML <dialog> element handles open and close natively, and a hidden checkbox with a label works in older browsers. Both are less flexible than JavaScript when you need focus trapping or an Escape key handler.