SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/modals

8 CSS Modals and Dialogs

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.

01. Native Dialog Modal

DialogAccessible

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>
.open { padding: 11px 18px; border: 0; border-radius: 10px; background: #dc2626; color: #fff; font: 700 15px Inter, system-ui, sans-serif; cursor: pointer; }
#md1 { width: min(380px, 90vw); padding: 24px; border: 0; border-radius: 16px; background: #fff; color: #475467; font: 15px/1.5 Inter, system-ui, sans-serif; box-shadow: 0 24px 60px -20px rgba(0,0,0,.6); }
#md1::backdrop { background: rgba(8,10,15,.6); backdrop-filter: blur(2px); }
#md1 h2 { margin: 0 0 8px; color: #101828; font-size: 19px; }
#md1 p { margin: 0 0 20px; }
.acts { display: flex; justify-content: flex-end; gap: 10px; }
.acts button { padding: 10px 16px; border-radius: 10px; font: 700 14px Inter, sans-serif; cursor: pointer; }
.ghost { border: 1.5px solid #d0d5dd; background: #fff; color: #344054; }
.danger { border: 0; background: #d92d20; color: #fff; }
.acts button:focus-visible, .open:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
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.

View code
<button class="sh-open" id="sh-open">Share</button>
<dialog class="sheet" id="sheet" aria-labelledby="sh-t">
  <span class="grab" aria-hidden="true"></span>
  <h2 id="sh-t">Share this product</h2>
  <div class="opts"><button>Copy link</button><button>Email</button><button>Message</button></div>
  <form method="dialog"><button class="close">Close</button></form>
</dialog>
.sh-open { padding: 11px 20px; border: 0; border-radius: 999px; background: #101828; color: #fff; font: 700 15px Inter, system-ui, sans-serif; cursor: pointer; }
.sheet { width: 100%; max-width: 480px; margin: auto auto 0; padding: 10px 20px 20px; border: 0; border-radius: 20px 20px 0 0; background: #fff; font-family: Inter, system-ui, sans-serif; transform: translateY(0); transition: transform .3s, display .3s allow-discrete, overlay .3s allow-discrete; }
@starting-style { .sheet[open] { transform: translateY(100%); } }
.sheet::backdrop { background: rgba(16,24,40,.45); }
.grab { display: block; width: 40px; height: 5px; margin: 0 auto 14px; border-radius: 3px; background: #d0d5dd; }
.sheet h2 { margin: 0 0 14px; color: #101828; font-size: 17px; }
.opts { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 14px; }
.opts button { padding: 14px 6px; border: 0; border-radius: 12px; background: #f2f4f7; color: #101828; font: 600 13px Inter, sans-serif; cursor: pointer; }
.close { width: 100%; padding: 12px; border: 1.5px solid #d0d5dd; border-radius: 12px; background: #fff; font: 700 14px Inter, sans-serif; cursor: pointer; }
@media (prefers-reduced-motion: reduce) { .sheet { transition: none; } }
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.

View code
<button class="dr-open" id="dr-open">Cart (2)</button>
<dialog class="drawer" id="drawer" aria-labelledby="dr-t">
  <header><h2 id="dr-t">Your cart</h2><form method="dialog"><button aria-label="Close cart">&times;</button></form></header>
  <ul>
    <li><span>Linen shirt</span><b>$48</b></li>
    <li><span>Clay mug</span><b>$22</b></li>
  </ul>
  <footer><p>Subtotal <b>$70</b></p><button class="pay">Checkout</button></footer>
</dialog>
.dr-open { padding: 11px 18px; border: 1px solid #343c51; border-radius: 10px; background: #151a26; color: #fff; font: 700 15px Inter, system-ui, sans-serif; cursor: pointer; }
.drawer { width: min(340px, 86vw); height: 100%; max-height: none; margin: 0 0 0 auto; padding: 0; border: 0; background: #12161f; color: #c9d1e2; font: 15px Inter, system-ui, sans-serif; transition: transform .3s, display .3s allow-discrete, overlay .3s allow-discrete; }
@starting-style { .drawer[open] { transform: translateX(100%); } }
.drawer::backdrop { background: rgba(0,0,0,.55); }
.drawer[open] { display: flex; flex-direction: column; }
.drawer header { display: flex; align-items: center; justify-content: space-between; padding: 18px 20px; border-bottom: 1px solid #262d3e; }
.drawer h2 { margin: 0; color: #fff; font-size: 18px; }
.drawer header button { width: 34px; height: 34px; border: 0; border-radius: 50%; background: #1e2433; color: #fff; font-size: 20px; cursor: pointer; }
.drawer ul { flex: 1; margin: 0; padding: 8px 20px; list-style: none; }
.drawer li { display: flex; justify-content: space-between; padding: 14px 0; border-bottom: 1px solid #1e2433; }
.drawer li b { color: #fff; }
.drawer footer { padding: 18px 20px; border-top: 1px solid #262d3e; }
.drawer footer p { display: flex; justify-content: space-between; margin: 0 0 12px; }
.pay { width: 100%; padding: 12px; border: 0; border-radius: 10px; background: #34d399; color: #062b1f; font: 700 15px Inter, sans-serif; cursor: pointer; }
@media (prefers-reduced-motion: reduce) { .drawer { transition: none; } }
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>
.ok-md { width: min(360px, 90vw); padding: 30px 26px 24px; border: 0; border-radius: 22px; background: #fff; text-align: center; font-family: Inter, system-ui, sans-serif; color: #475467; }
.ok-md::backdrop { background: rgba(16,24,40,.4); }
.ok-ic { position: relative; width: 64px; height: 64px; margin: 0 auto 16px; border-radius: 50%; background: #dcfce7; }
.ok-ic::after { content: ""; position: absolute; left: 24px; top: 14px; width: 13px; height: 26px; border: solid #15803d; border-width: 0 4px 4px 0; transform: rotate(45deg); }
.ok-md h2 { margin: 0 0 8px; color: #101828; font-size: 21px; }
.ok-md p { margin: 0 0 22px; font-size: 14.5px; line-height: 1.55; }
.ok-md button { width: 100%; padding: 12px; border: 0; border-radius: 12px; background: #15803d; color: #fff; font: 700 15px Inter, sans-serif; cursor: pointer; }
.ok-md button:focus-visible { outline: 2px solid #14532d; outline-offset: 3px; }
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">&times;</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>
.nl { width: min(560px, 94vw); padding: 0; border: 0; overflow: hidden; border-radius: 18px; background: #12161f; color: #aab3c7; font: 14px Inter, system-ui, sans-serif; }
.nl[open] { display: grid; grid-template-columns: 1fr 1.3fr; }
.nl::backdrop { background: rgba(0,0,0,.6); }
.art { background: radial-gradient(circle at 30% 30%, #f59e0b, transparent 55%), radial-gradient(circle at 70% 70%, #ec4899, transparent 55%), #1b1630; }
.body { position: relative; display: grid; gap: 8px; padding: 30px 24px 24px; }
.x { position: absolute; top: 10px; right: 10px; width: 32px; height: 32px; border: 0; border-radius: 50%; background: #1e2433; color: #fff; font-size: 18px; cursor: pointer; }
.nl h2 { margin: 0; color: #fff; font-size: 20px; line-height: 1.25; }
.nl p { margin: 0 0 6px; }
.nl label { color: #c9d1e2; font-weight: 600; }
.nl input { padding: 11px 12px; border: 1px solid #343c51; border-radius: 10px; background: #0d1018; color: #fff; font-size: 15px; outline: none; }
.nl input:focus { border-color: #f59e0b; }
.join { padding: 12px; border: 0; border-radius: 10px; background: #f59e0b; color: #1a1205; font: 700 15px Inter, sans-serif; cursor: pointer; }
.nl button:focus-visible { outline: 2px solid #fcd34d; outline-offset: 2px; }
@media (max-width: 520px) { .nl[open] { grid-template-columns: 1fr; } .art { height: 90px; } }
document.getElementById('nl').showModal();

07. Image Lightbox

GalleryDialog

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">&times;</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>
.thumb { width: 160px; height: 110px; border: 0; border-radius: 12px; background: linear-gradient(180deg, #f97316 0%, #db2777 45%, #1e1b4b 100%); cursor: zoom-in; }
.thumb:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
.lb { position: relative; width: min(620px, 92vw); padding: 0; border: 0; background: transparent; color: #d6dcea; font: 14px Inter, system-ui, sans-serif; text-align: center; }
.lb::backdrop { background: rgba(4,6,10,.85); }
.pic { aspect-ratio: 4 / 3; border-radius: 12px; background: linear-gradient(180deg, #f97316 0%, #db2777 45%, #1e1b4b 100%); }
.lb p { margin: 12px 0 0; }
.lb-x { position: absolute; top: -14px; right: -14px; z-index: 1; width: 38px; height: 38px; border: 0; border-radius: 50%; background: #fff; color: #111; font-size: 22px; cursor: pointer; }
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>
.ob { width: min(380px, 90vw); padding: 26px; border: 0; border-radius: 20px; background: #fff; font-family: Inter, system-ui, sans-serif; color: #475467; }
.ob::backdrop { background: rgba(16,24,40,.45); }
.count { margin: 0 0 6px; color: #6941c6; font-size: 13px; font-weight: 700; }
.ob h2 { margin: 0 0 8px; color: #101828; font-size: 20px; outline: none; }
#ob-p { margin: 0 0 18px; font-size: 14.5px; line-height: 1.55; }
.dots { display: flex; gap: 6px; margin-bottom: 18px; }
.dots i { width: 8px; height: 8px; border-radius: 50%; background: #e4e7ec; transition: width .2s; }
.dots i.on { width: 22px; border-radius: 4px; background: #7f56d9; }
.nav { display: flex; justify-content: space-between; }
.nav button { padding: 10px 18px; border: 1.5px solid #d0d5dd; border-radius: 10px; background: #fff; color: #344054; font: 700 14px Inter, sans-serif; cursor: pointer; }
.nav button:disabled { opacity: .5; cursor: default; }
.nav .nx { border-color: #6941c6; background: #6941c6; color: #fff; }
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.