0 Pure CSS8 with JSPublished 2026-10-04Free · no sign-up
Eight modal patterns built on the native HTML <dialog> element, which handles focus trapping, the Escape key and a backdrop for you. The set covers a delete confirmation, a mobile bottom sheet, a side drawer, a success message, a newsletter prompt, a cookie banner, an image lightbox and a three-step onboarding flow.
A modal built on the HTML dialog element. showModal() makes the rest of the page inert, traps focus, closes on Escape and draws a ::backdrop, so the accessibility work is done by the browser. Clicking the dimmed backdrop also closes it.
View code
<button class="open" id="md1-open">Delete project</button>
<dialog id="md1" aria-labelledby="md1-t">
<h2 id="md1-t">Delete this project?</h2>
<p>"Spring catalogue" and its 24 files will be removed. This cannot be undone.</p>
<form method="dialog" class="acts">
<button value="cancel" class="ghost">Cancel</button>
<button value="delete" class="danger">Delete</button>
</form>
</dialog>
const d = document.getElementById('md1');
document.getElementById('md1-open').addEventListener('click', () => d.showModal());
d.addEventListener('click', e => { if (e.target === d) d.close(); });
d.showModal();
02. Bottom Sheet
MobileDialog
A mobile-style bottom sheet that slides up from the bottom edge. It is still a dialog element, so focus and Escape behave correctly, and the slide-in uses a CSS @starting-style transition that falls back to simply appearing in older browsers.
const s = document.getElementById('sheet');
document.getElementById('sh-open').addEventListener('click', () => s.showModal());
s.addEventListener('click', e => { if (e.target === s) s.close(); });
s.showModal();
03. Side Drawer Panel
DrawerDialog
A panel that slides in from the right for filters, a cart or settings. It reuses the dialog element pinned to the right edge with full height, so the page behind is inert while it is open and Escape closes it.
const dr = document.getElementById('drawer');
document.getElementById('dr-open').addEventListener('click', () => dr.showModal());
dr.addEventListener('click', e => { if (e.target === dr) dr.close(); });
dr.showModal();
04. Success Confirmation Modal
FeedbackDialog
A celebratory confirmation after an order: a large check icon, a short message and one clear next step. The icon is drawn with CSS borders inside a tinted circle, and the single button receives focus when the dialog opens.
View code
<dialog class="ok-md" id="okmd" aria-labelledby="ok-t">
<div class="ok-ic" aria-hidden="true"></div>
<h2 id="ok-t">Order placed</h2>
<p>We have emailed your receipt. Your parcel leaves our studio within two working days.</p>
<form method="dialog"><button autofocus>Track my order</button></form>
</dialog>
const ok = document.getElementById('okmd');
ok.showModal();
05. Newsletter Signup Modal
MarketingForm
A two-column newsletter prompt with an image area and a short email form. It closes from a visible close button, the Escape key or the backdrop, and the email field has a real label and autocomplete, not only a placeholder.
View code
<dialog class="nl" id="nl" aria-labelledby="nl-t">
<div class="art" aria-hidden="true"></div>
<form method="dialog" class="body">
<button class="x" value="close" aria-label="Close">×</button>
<h2 id="nl-t">Get 10% off your first order</h2>
<p>New pieces every month, no spam.</p>
<label for="nl-e">Email</label>
<input id="nl-e" type="email" autocomplete="email">
<button value="join" class="join">Join the list</button>
</form>
</dialog>
A non-blocking consent panel pinned to the bottom corner, using a non-modal dialog so the page stays usable. Accept and Reject are equal in size and weight, which privacy regulators expect, and a link opens detailed settings.
View code
<dialog class="ck" open aria-labelledby="ck-t">
<h2 id="ck-t">Cookies on this site</h2>
<p>We use cookies to remember your cart and to measure how the site is used. <a href="#">Choose settings</a></p>
<div class="row"><button>Reject all</button><button>Accept all</button></div>
</dialog>
A lightbox that opens a larger view of a thumbnail with its caption. The picture here is a CSS gradient stand-in so the demo has no external image; swap in an img with real alt text. The close button sits in a predictable top-right spot.
View code
<button class="thumb" id="lb-open" aria-label="Open image: harbour at dusk"></button>
<dialog class="lb" id="lb" aria-label="Harbour at dusk">
<form method="dialog"><button class="lb-x" aria-label="Close image">×</button></form>
<div class="pic" role="img" aria-label="Boats moored in a harbour at dusk"></div>
<p>Harbour at dusk, print 30 x 40 cm</p>
</dialog>
const lb = document.getElementById('lb');
document.getElementById('lb-open').addEventListener('click', () => lb.showModal());
lb.addEventListener('click', e => { if (e.target === lb) lb.close(); });
08. Multi-Step Onboarding Modal
OnboardingJavaScript
A three-step welcome modal with progress dots, Back and Next buttons and a final Done. The current step is announced as Step 1 of 3, the dots are decorative, and focus returns to the step heading each time the step changes.
View code
<dialog class="ob" id="ob" aria-labelledby="ob-h">
<p class="count" id="ob-c">Step 1 of 3</p>
<h2 id="ob-h" tabindex="-1">Add your first product</h2>
<p id="ob-p">Upload a photo, set a price and you are ready to sell.</p>
<div class="dots" aria-hidden="true"><i class="on"></i><i></i><i></i></div>
<div class="nav"><button id="ob-back" disabled>Back</button><button id="ob-next" class="nx">Next</button></div>
</dialog>
const steps = [
['Add your first product', 'Upload a photo, set a price and you are ready to sell.'],
['Connect payments', 'Link a payout account so money from orders reaches you.'],
['Share your shop', 'Post your shop link on social media or send it to friends.']
];
let i = 0;
const ob = document.getElementById('ob'), back = document.getElementById('ob-back'), next = document.getElementById('ob-next');
function show() {
document.getElementById('ob-c').textContent = 'Step ' + (i + 1) + ' of 3';
document.getElementById('ob-h').textContent = steps[i][0];
document.getElementById('ob-p').textContent = steps[i][1];
document.querySelectorAll('.dots i').forEach((d, k) => d.classList.toggle('on', k === i));
back.disabled = i === 0;
next.textContent = i === 2 ? 'Done' : 'Next';
document.getElementById('ob-h').focus();
}
next.addEventListener('click', () => { if (i === 2) { ob.close(); return; } i++; show(); });
back.addEventListener('click', () => { i--; show(); });
ob.showModal();
About these CSS Modals and Dialogs
Use the dialog element
dialog.showModal() opens a true modal: the rest of the page becomes inert, focus moves inside, Escape closes it and a ::backdrop pseudo-element covers the page. That replaces dozens of lines of custom focus-trap JavaScript, and it is supported in every current browser.
Closing it
Give every modal a visible way out: a Cancel or Close button inside a form method="dialog" closes it without JavaScript. Closing on a backdrop click is a nice extra, done by checking whether the click target is the dialog element itself.
Modal or not?
Use showModal() when the person must respond before continuing, like confirming a deletion. For cookie banners and other messages that should not block the page, use a non-modal dialog with the open attribute, as in the cookie demo.
Animating in
The bottom sheet and drawer slide in using @starting-style together with transition-behavior: allow-discrete on display and overlay. Browsers without support simply show the dialog straight away, and reduced-motion users get no slide.
Frequently asked questions
Do these modals need JavaScript?
Opening a modal with showModal() needs one line of JavaScript; closing works with form method="dialog" alone.
Is the dialog element accessible?
Yes. Give it a name with aria-labelledby pointing at its heading, and the browser handles focus and inert content.
Why do the demos open on load?
So you can see them in the preview. In your site, call showModal() from a button click instead.