Accessible Modal Dialog with HTML, CSS, and Vanilla JavaScript
Run it. Change it. Make it yours.
One-file front-end editor
Write plain HTML, CSS, JavaScript, or CDN-powered Vue and React demos.
What This Snippet Does
This snippet shows how to build a reusable accessible modal dialog using HTML, CSS, and vanilla JavaScript. It includes keyboard support, focus trapping, ARIA attributes, overlay click handling, and focus restoration when the modal closes.
Real-World Use Case
Use this modal when you need to show important content without sending users to a new page. Common examples include newsletter signup forms, delete confirmations, login prompts, onboarding tips, terms and conditions notices, and checkout warnings.
Why Accessibility Matters
A modal should work for mouse users, keyboard users, and screen reader users. A visually working popup is not enough. Users must be able to open it, understand it, navigate inside it, close it, and return to the correct place afterward.
Key Concepts
- role="dialog" identifies the modal as a dialog for assistive technologies.
- aria-modal="true" tells screen readers that the rest of the page is inactive while the modal is open.
- aria-labelledby connects the dialog to its visible title.
- aria-describedby connects the dialog to supporting description text.
- Focus trapping keeps keyboard focus inside the modal while it is open.
- Escape key support gives users a familiar way to close the dialog.
- Focus restoration sends users back to the button or element that opened the modal.
Complete Snippet Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accessible Modal Dialog</title>
<style>
:root {
--modal-radius: 12px;
--modal-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
--modal-overlay: rgba(15, 23, 42, 0.65);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: Arial, sans-serif;
line-height: 1.5;
color: #1f2937;
background: #f8fafc;
padding: 2rem;
}
button,
input {
font: inherit;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
border-radius: 8px;
padding: 0.75rem 1rem;
cursor: pointer;
background: #2563eb;
color: #ffffff;
font-weight: 700;
}
.button:hover {
background: #1d4ed8;
}
.button:focus-visible,
.modal-close:focus-visible,
input:focus-visible {
outline: 3px solid #93c5fd;
outline-offset: 3px;
}
.modal-overlay {
position: fixed;
inset: 0;
z-index: 1000;
display: none;
align-items: center;
justify-content: center;
padding: 1rem;
background: var(--modal-overlay);
}
.modal-overlay.is-open {
display: flex;
}
.modal {
width: min(100%, 520px);
max-height: min(90vh, 700px);
overflow-y: auto;
border-radius: var(--modal-radius);
background: #ffffff;
box-shadow: var(--modal-shadow);
padding: 1.5rem;
}
.modal-header {
display: flex;
align-items: start;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
}
.modal-title {
margin: 0;
font-size: 1.35rem;
line-height: 1.2;
}
.modal-close {
border: 0;
background: transparent;
color: #374151;
cursor: pointer;
font-size: 1.75rem;
line-height: 1;
padding: 0.25rem;
}
.modal-description {
margin-top: 0;
color: #4b5563;
}
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.4rem;
font-weight: 700;
}
input[type="email"] {
width: 100%;
border: 1px solid #cbd5e1;
border-radius: 8px;
padding: 0.75rem;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 1.25rem;
}
.button-secondary {
background: #e5e7eb;
color: #111827;
}
.button-secondary:hover {
background: #d1d5db;
}
</style>
</head>
<body>
<main>
<h1>Accessible Modal Dialog Demo</h1>
<p>Click the button below to open a keyboard-friendly modal dialog.</p>
<button class="button" type="button" data-open-modal>
Open Newsletter Modal
</button>
</main>
<div
class="modal-overlay"
id="newsletterModal"
role="dialog"
aria-modal="true"
aria-labelledby="modalTitle"
aria-describedby="modalDescription"
aria-hidden="true"
>
<div class="modal" role="document">
<div class="modal-header">
<h2 class="modal-title" id="modalTitle">Subscribe to our newsletter</h2>
<button class="modal-close" type="button" data-close-modal aria-label="Close modal">
×
</button>
</div>
<p class="modal-description" id="modalDescription">
Get practical coding tips, accessibility notes, and frontend snippets delivered to your inbox.
</p>
<form id="newsletterForm">
<div class="form-group">
<label for="email">Email address</label>
<input id="email" name="email" type="email" autocomplete="email" placeholder="you@example.com" required>
</div>
<div class="modal-actions">
<button class="button button-secondary" type="button" data-close-modal>
Cancel
</button>
<button class="button" type="submit">
Subscribe
</button>
</div>
</form>
</div>
</div>
<script>
const openModalButton = document.querySelector('[data-open-modal]');
const modalOverlay = document.getElementById('newsletterModal');
const closeModalButtons = modalOverlay.querySelectorAll('[data-close-modal]');
const form = document.getElementById('newsletterForm');
const focusableSelectors = [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(',');
let previouslyFocusedElement = null;
function getFocusableElements() {
return Array.from(modalOverlay.querySelectorAll(focusableSelectors));
}
function openModal() {
previouslyFocusedElement = document.activeElement;
modalOverlay.classList.add('is-open');
modalOverlay.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
const focusableElements = getFocusableElements();
if (focusableElements.length) {
focusableElements[0].focus();
}
document.addEventListener('keydown', handleModalKeydown);
}
function closeModal() {
modalOverlay.classList.remove('is-open');
modalOverlay.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
document.removeEventListener('keydown', handleModalKeydown);
if (previouslyFocusedElement) {
previouslyFocusedElement.focus();
}
}
function handleModalKeydown(event) {
if (event.key === 'Escape') {
event.preventDefault();
closeModal();
return;
}
if (event.key !== 'Tab') return;
const focusableElements = getFocusableElements();
if (!focusableElements.length) return;
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault();
lastElement.focus();
}
if (!event.shiftKey && document.activeElement === lastElement) {
event.preventDefault();
firstElement.focus();
}
}
openModalButton.addEventListener('click', openModal);
closeModalButtons.forEach((button) => {
button.addEventListener('click', closeModal);
});
modalOverlay.addEventListener('click', (event) => {
if (event.target === modalOverlay) {
closeModal();
}
});
form.addEventListener('submit', (event) => {
event.preventDefault();
alert('Thanks for subscribing!');
closeModal();
});
</script>
</body>
</html>
How It Works
- The open button uses
data-open-modalso JavaScript can find it without relying on styling classes. - The modal wrapper uses
role="dialog",aria-modal="true",aria-labelledby, andaria-describedbyto describe the dialog to assistive technologies. - The modal starts hidden with
aria-hidden="true"and without theis-openclass. - When the modal opens, JavaScript stores the previously focused element, shows the modal, disables page scrolling, and moves focus into the dialog.
- The
keydownlistener handlesEscape,Tab, andShift + Tabwhile the modal is open. - When the modal closes, the listener is removed, page scrolling is restored, and focus returns to the original trigger button.
Customization Notes
- Change the modal width by editing
width: min(100%, 520px);inside the.modalrule. - Change the overlay darkness by editing the
--modal-overlayCSS variable. - Add entrance animations with
opacityandtransform, but avoid animations that make the modal hard to use. - Replace the newsletter form with a login form, confirmation message, settings panel, or alert content.
- For destructive actions, make the safest action the easiest to reach and use clear button labels such as
CancelandDelete project.
Common Mistakes
- Using a modal without moving focus into it when it opens.
- Forgetting to return focus to the trigger element when the modal closes.
- Using only mouse click handlers and ignoring keyboard users.
- Leaving the background page scrollable while the modal is open.
- Using vague button labels like
OKwhen a clearer action label would help users. - Putting the modal title in the design but not connecting it with
aria-labelledby. - Removing the visible focus outline, which makes keyboard navigation difficult.
Beginner-Friendly Tips
- Keep the modal HTML near the end of the page so it is easy to find and maintain.
- Use data attributes such as
data-open-modalanddata-close-modalfor JavaScript hooks. - Test the modal without touching your mouse. Open it, tab through it, submit or cancel it, and close it with Escape.
- Use real labels for form fields instead of relying only on placeholders.
Related Practice Ideas
- Convert this modal into a reusable JavaScript class.
- Create multiple modals on the same page using different data attributes.
- Add a confirmation modal before deleting an item from a list.
- Add smooth open and close animations while respecting
prefers-reduced-motion. - Build the same accessible modal pattern in React, Vue, or Laravel Blade.
Support
Keep CompileQuestHub free
If this reusable code solution helped you, support more open tutorials and code examples.
Need More?
Request a topic or report an issue
Use the contact form to request follow-up tutorials or report broken code, missing files, or outdated links.
Page Info
Freshness and topics
Topic: Accessible Modal Dialog
Difficulty: Intermediate
Reading time: 8 min read
Published: 6/6/2026
Updated: 7/31/2026
Before You Start
Prerequisites
- Basic HTML knowledge
- Fundamental CSS styling
- Introductory JavaScript understanding
Outcome
What you will learn
- Create an accessible modal dialog using semantic HTML and ARIA attributes
- Implement focus trapping and restoration with vanilla JavaScript
- Add keyboard support for Escape and Tab navigation
- Style the modal and overlay with modern CSS
- Integrate the modal component into any web page
Learning Path
Accessible UI Components
Continue this sequence from the series page and move through the lessons in order.
Open series