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
- Sit above every other element, dimming the page behind it with an overlay.
- Be clearly announced to assistive technology as a dialog, not just a styled div.
- Offer more than one way to close - a visible button at minimum, plus Escape.
- 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'>×</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>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.
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 === overlaystops 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.