<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<symbol id="mo-i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/></symbol>
<symbol id="mo-i-invite" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="9.5" cy="8" r="3.5"/><path d="M3.5 19.5c.7-3.4 3-5 6-5s5.3 1.6 6 5M18.5 8v6M15.5 11h6"/></symbol>
<symbol id="mo-i-pulse" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 12h4l2.5-6.5 4 13 2.5-6.5h4"/></symbol>
<symbol id="mo-i-cookie" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20.5 12.5a8.5 8.5 0 1 1-9-9 3 3 0 0 0 3.5 3.5 3 3 0 0 0 3 3 2.5 2.5 0 0 0 2.5 2.5z"/><path d="M8.5 9.5h.01M9.5 15h.01M14.5 14.5h.01" stroke-width="2.6"/></symbol>
</svg>
<div><span class="lbl">Zoom and fade</span>
<div class="btns"><button class="mo-btn is-primary" type="button" commandfor="mo-invite" command="show-modal" aria-haspopup="dialog"><svg aria-hidden="true"><use href="#mo-i-invite"/></svg>Invite people</button></div></div>
<div><span class="lbl">Slide-over panel</span>
<div class="btns"><button class="mo-btn" type="button" commandfor="mo-activity" command="show-modal" aria-haspopup="dialog"><svg aria-hidden="true"><use href="#mo-i-pulse"/></svg>Activity</button></div></div>
<div><span class="lbl">Pop-up sheet</span>
<div class="btns"><button class="mo-btn" type="button" commandfor="mo-cookies" command="show-modal" aria-haspopup="dialog"><svg aria-hidden="true"><use href="#mo-i-cookie"/></svg>Cookie settings</button></div></div>
<!-- 1. Zoom and fade. closedby="closerequest": Esc closes it, a stray click outside does not, so a half-typed address survives. -->
<dialog class="mo mo-zoom" id="mo-invite" closedby="closerequest" aria-labelledby="mo-invite-title">
<div class="mo-head">
<span class="mo-icon" aria-hidden="true"><svg><use href="#mo-i-invite"/></svg></span>
<div class="mo-titles"><h2 class="mo-title" id="mo-invite-title">Invite people to Northfield</h2><p class="mo-sub">They get an email with a link to join the workspace.</p></div>
<button class="mo-x" type="button" commandfor="mo-invite" command="close" aria-label="Close"><svg aria-hidden="true"><use href="#mo-i-x"/></svg></button>
</div>
<div class="mo-body">
<form class="mo-invite" method="dialog">
<div class="mo-field"><label for="mo-invite-email">Email address</label><input class="mo-input" id="mo-invite-email" name="email" type="email" placeholder="[email protected]" autocomplete="off" required autofocus></div>
<div class="mo-field"><label for="mo-invite-role">Role</label><select class="mo-input" id="mo-invite-role" name="role"><option>Viewer</option><option selected>Editor</option><option>Admin</option></select></div>
<button class="mo-btn is-primary" value="invite">Send invite</button>
</form>
<h3 class="mo-h3">People with access</h3>
<ul class="mo-people">
<li><img src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&crop=faces&w=96&h=96&q=70" width="38" height="38" alt="Hana Kobayashi"><div class="mo-person"><b>Hana Kobayashi</b><span>[email protected]</span></div><span class="mo-role is-owner">Owner</span></li>
<li><img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&crop=faces&w=96&h=96&q=70" width="38" height="38" alt="Theo Marchetti"><div class="mo-person"><b>Theo Marchetti</b><span>[email protected]</span></div><span class="mo-role">Editor</span></li>
<li><img src="https://images.unsplash.com/photo-1580489944761-15a19d654956?auto=format&fit=crop&crop=faces&w=96&h=96&q=70" width="38" height="38" alt="Priya Raman"><div class="mo-person"><b>Priya Raman</b><span>[email protected]</span></div><span class="mo-role">Viewer</span></li>
</ul>
</div>
<div class="mo-foot">
<p class="mo-note">3 of 10 seats used</p>
<button class="mo-btn" type="button" commandfor="mo-invite" command="close">Done</button>
</div>
</dialog>
<!-- 2. Slide-over panel. closedby="any": light dismiss by a backdrop click or Esc, nothing to lose. -->
<dialog class="mo mo-panel" id="mo-activity" closedby="any" aria-labelledby="mo-activity-title">
<div class="mo-head">
<div class="mo-titles"><h2 class="mo-title" id="mo-activity-title">Activity</h2><p class="mo-sub">Northfield Ceramics · 3 unread</p></div>
<button class="mo-x" type="button" commandfor="mo-activity" command="close" aria-label="Close"><svg aria-hidden="true"><use href="#mo-i-x"/></svg></button>
</div>
<div class="mo-feed" tabindex="0" role="region" aria-label="Activity feed">
<h3 class="mo-h3">Today</h3>
<ol class="mo-events">
<li class="is-new"><span class="mo-av"><img src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&crop=faces&w=96&h=96&q=70" width="36" height="36" alt="Hana Kobayashi"></span><div class="mo-event"><p><span class="mo-sr">Unread: </span><b>Hana Kobayashi</b> published <strong>Autumn glaze catalogue</strong></p><time datetime="2026-09-25T09:42">9:42</time></div></li>
<li class="is-new"><span class="mo-av"><img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&crop=faces&w=96&h=96&q=70" width="36" height="36" alt="Theo Marchetti"></span><div class="mo-event"><p><span class="mo-sr">Unread: </span><b>Theo Marchetti</b> commented on <strong>Kiln schedule, week 40</strong></p><p class="mo-quote">Firing moves to Thursday. The kiln is booked for the tile order on Wednesday.</p><time datetime="2026-09-25T09:15">9:15</time></div></li>
<li class="is-new"><span class="mo-av"><img src="https://images.unsplash.com/photo-1580489944761-15a19d654956?auto=format&fit=crop&crop=faces&w=96&h=96&q=70" width="36" height="36" alt="Priya Raman"></span><div class="mo-event"><p><span class="mo-sr">Unread: </span><b>Priya Raman</b> uploaded 12 photos to <strong>Stoneware mugs</strong></p><time datetime="2026-09-25T08:58">8:58</time></div></li>
<li><span class="mo-av" aria-hidden="true">JW</span><div class="mo-event"><p><b>Jonas Weber</b> invited [email protected] as a Viewer</p><time datetime="2026-09-25T08:31">8:31</time></div></li>
<li><span class="mo-av"><img src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&crop=faces&w=96&h=96&q=70" width="36" height="36" alt="Hana Kobayashi"></span><div class="mo-event"><p><b>Hana Kobayashi</b> changed the price of <strong>Speckled bowl, large</strong> to £34</p><time datetime="2026-09-25T08:03">8:03</time></div></li>
</ol>
<h3 class="mo-h3">Yesterday</h3>
<ol class="mo-events">
<li><span class="mo-av"><img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&crop=faces&w=96&h=96&q=70" width="36" height="36" alt="Theo Marchetti"></span><div class="mo-event"><p><b>Theo Marchetti</b> completed <strong>Order 1187: pack and ship</strong></p><time datetime="2026-09-24T17:40">17:40</time></div></li>
<li><span class="mo-av" aria-hidden="true">AM</span><div class="mo-event"><p><b>Ama Mensah</b> joined the workspace as an Editor</p><time datetime="2026-09-24T16:12">16:12</time></div></li>
<li><span class="mo-av"><img src="https://images.unsplash.com/photo-1580489944761-15a19d654956?auto=format&fit=crop&crop=faces&w=96&h=96&q=70" width="36" height="36" alt="Priya Raman"></span><div class="mo-event"><p><b>Priya Raman</b> archived <strong>Summer market 2026</strong></p><time datetime="2026-09-24T11:05">11:05</time></div></li>
</ol>
</div>
<form class="mo-foot" method="dialog">
<a class="mo-btn" href="#">Full log</a>
<button class="mo-btn is-primary" value="read">Mark all as read</button>
</form>
</dialog>
<!-- 3. Pop-up sheet. closedby="closerequest": no light dismiss, because a click on the page behind is not a consent choice.
Esc still closes it, and saves nothing. -->
<dialog class="mo mo-sheet" id="mo-cookies" closedby="closerequest" aria-labelledby="mo-cookies-title" aria-describedby="mo-cookies-desc">
<form class="mo-form" method="dialog">
<div class="mo-head">
<span class="mo-icon" aria-hidden="true"><svg><use href="#mo-i-cookie"/></svg></span>
<div class="mo-titles"><h2 class="mo-title" id="mo-cookies-title">Cookie settings</h2><p class="mo-sub" id="mo-cookies-desc">Choose which cookies Northfield Ceramics may use. You can change this at any time from the link in the footer.</p></div>
</div>
<div class="mo-body">
<fieldset class="mo-choices">
<legend class="mo-sr">Cookie categories</legend>
<div class="mo-choice">
<div class="mo-choice-text"><label for="mo-ck-necessary">Necessary <span class="mo-pill">Always on</span></label><p id="mo-ck-necessary-desc">Keep you signed in, remember your basket and protect the checkout.</p></div>
<input class="mo-switch" type="checkbox" role="switch" id="mo-ck-necessary" aria-describedby="mo-ck-necessary-desc" checked disabled>
</div>
<div class="mo-choice">
<div class="mo-choice-text"><label for="mo-ck-analytics">Analytics</label><p id="mo-ck-analytics-desc">Count visits and see which pages are read, so we can fix what is slow or confusing.</p></div>
<input class="mo-switch" type="checkbox" role="switch" id="mo-ck-analytics" name="analytics" aria-describedby="mo-ck-analytics-desc">
</div>
<div class="mo-choice">
<div class="mo-choice-text"><label for="mo-ck-marketing">Marketing</label><p id="mo-ck-marketing-desc">Show our ads on other sites and measure whether they lead to an order.</p></div>
<input class="mo-switch" type="checkbox" role="switch" id="mo-ck-marketing" name="marketing" aria-describedby="mo-ck-marketing-desc">
</div>
</fieldset>
</div>
<div class="mo-foot">
<button class="mo-btn" value="reject">Reject all</button>
<button class="mo-btn is-primary" value="save">Save choices</button>
</div>
</form>
</dialog>
CSS
/* Modern CSS modals: the native <dialog> doing with HTML and CSS what usually needs JavaScript.
- Opened by invoker commands: <button commandfor="id" command="show-modal">. Closed by command="close" or <form method="dialog">.
- closedby="any" adds light dismiss (backdrop click + Esc); closedby="closerequest" allows Esc only.
- Enter AND exit animations in CSS alone: @starting-style gives the entry state, and transition-behavior:allow-discrete
on display and overlay keeps the dialog (and its ::backdrop) in the top layer until the exit transition ends.
@starting-style is keyed to :modal, so a static copy with [open] (e.g. a preview) renders in its final state. */
html{scrollbar-gutter:stable} /* the page does not shift sideways when the scroll lock hides its scrollbar */
html:has(.mo:modal){overflow:hidden} /* scroll lock while one of these dialogs is open */
.mo-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.mo-btn{--mo-accent:#c2410c;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:42px;padding:9px 16px;border:1px solid #d6d3d1;border-radius:10px;
background:#fff;color:#1c1917;font-family:inherit;font-size:.9rem;font-weight:600;line-height:1.2;text-decoration:none;white-space:nowrap;cursor:pointer;transition:background-color .15s,border-color .15s,box-shadow .15s}
.mo-btn:hover{background:#f5f5f4;border-color:#a8a29e}
.mo-btn:focus-visible{outline:2px solid var(--mo-accent);outline-offset:2px}
.mo-btn.is-primary{background:var(--mo-accent);border-color:var(--mo-accent);color:#fff;box-shadow:0 6px 16px -8px rgba(234,88,12,.8)}
.mo-btn.is-primary:hover{background:#9a3412;border-color:#9a3412}
.mo-btn svg{width:18px;height:18px;flex:none}
/* The dialog shell, shared by all three variants */
.mo{--mo-accent:#c2410c;--mo-ink:#1c1917;--mo-muted:#57534e;--mo-line:#e7e5e4;--mo-soft:#fafaf9;--mo-dur:.26s;--mo-ease:cubic-bezier(.2,.8,.2,1);
box-sizing:border-box;flex-direction:column;max-width:none;padding:0;border:0;background:#fff;color:var(--mo-ink);font-size:.95rem;line-height:1.55;overflow:hidden;
box-shadow:0 0 0 1px rgba(28,25,23,.06),0 30px 70px -22px rgba(28,25,23,.5);
transition:opacity var(--mo-dur) var(--mo-ease),scale var(--mo-dur) var(--mo-ease),translate var(--mo-dur) var(--mo-ease),
display var(--mo-dur) allow-discrete,overlay var(--mo-dur) allow-discrete}
.mo *,.mo *::before,.mo *::after{box-sizing:border-box}
.mo[open]{display:flex}
.mo::backdrop{background-color:rgb(28 25 23 / 0);backdrop-filter:blur(0);
transition:background-color .26s,backdrop-filter .26s,display .26s allow-discrete,overlay .26s allow-discrete}
.mo[open]::backdrop{background-color:rgb(28 25 23 / .5);backdrop-filter:blur(3px)}
@starting-style{.mo:modal::backdrop{background-color:rgb(28 25 23 / 0);backdrop-filter:blur(0)}}
.mo-form{display:flex;flex-direction:column;flex:1;min-height:0;margin:0}
.mo-head{display:flex;align-items:flex-start;gap:14px;padding:22px 24px 6px}
.mo-titles{flex:1;min-width:0}
.mo-title{margin:0;font-size:1.15rem;font-weight:700;letter-spacing:-.01em;line-height:1.35}
.mo-sub{margin:3px 0 0;color:var(--mo-muted);font-size:.85rem}
.mo-icon{flex:none;display:grid;place-items:center;width:42px;height:42px;border-radius:12px;background:linear-gradient(135deg,#fff7ed,#ffedd5);color:var(--mo-accent)}
.mo-icon svg{width:22px;height:22px}
.mo-x{flex:none;display:grid;place-items:center;width:36px;height:36px;margin:-4px -8px 0 0;padding:0;border:0;border-radius:10px;background:transparent;color:var(--mo-muted);cursor:pointer;
transition:background-color .15s,color .15s}
.mo-x:hover{background:#f5f5f4;color:var(--mo-ink)}
.mo-x:focus-visible{outline:2px solid var(--mo-accent);outline-offset:1px}
.mo-x svg{width:20px;height:20px}
.mo-body{padding:16px 24px 22px}
.mo-foot{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:10px;margin:0;padding:14px 24px;border-top:1px solid var(--mo-line);background:var(--mo-soft)}
.mo-note{margin:0 auto 0 0;color:var(--mo-muted);font-size:.85rem}
.mo-h3{margin:0 0 6px;color:var(--mo-muted);font-size:.72rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase}
/* 1. Zoom and fade: scales up from .94 and fades in, and does the reverse on close */
.mo-zoom{position:fixed;inset:0;width:min(500px,calc(100% - 32px));height:fit-content;max-height:calc(100dvh - 48px);margin:auto;border-radius:18px;
overflow-y:auto;overscroll-behavior:contain;opacity:0;scale:.94}
.mo-zoom[open]{opacity:1;scale:1}
@starting-style{.mo-zoom:modal{opacity:0;scale:.94}}
.mo-invite{display:grid;grid-template-columns:minmax(0,1fr) 118px auto;align-items:end;gap:10px;margin:0 0 22px}
.mo-field label{display:block;margin:0 0 6px;color:var(--mo-ink);font-size:.8rem;font-weight:600}
.mo-input{display:block;width:100%;height:42px;margin:0;padding:0 12px;border:1px solid #8c857f;border-radius:10px;background-color:#fff;color:var(--mo-ink);font:inherit;font-size:.9rem}
.mo-input::placeholder{color:#78716c;opacity:1}
.mo-input:focus-visible{outline:2px solid var(--mo-accent);outline-offset:1px;border-color:var(--mo-accent)}
select.mo-input{appearance:none;padding-right:34px;cursor:pointer;
background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2357534e' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m7 10 5 5 5-5'/%3E%3C/svg%3E") no-repeat right 10px center/16px}
.mo-people{list-style:none;margin:0;padding:0}
.mo-people li{display:flex;align-items:center;gap:12px;padding:10px 0;border-top:1px solid var(--mo-line)}
.mo-people img{flex:none;width:38px;height:38px;border-radius:50%;object-fit:cover}
.mo-person{flex:1;min-width:0;font-size:.875rem;line-height:1.35}
.mo-person b{display:block;font-weight:600}
.mo-person span{display:block;overflow:hidden;color:var(--mo-muted);font-size:.8rem;text-overflow:ellipsis;white-space:nowrap}
.mo-role{flex:none;padding:3px 10px;border-radius:999px;background:#f5f5f4;color:#44403c;font-size:.75rem;font-weight:600}
.mo-role.is-owner{background:#ffedd5;color:#9a3412}
/* 2. Slide-over panel: full height from the right, slides in and back out */
.mo-panel{--mo-dur:.34s;--mo-ease:cubic-bezier(.3,0,.2,1);position:fixed;inset:0 0 0 auto;width:min(420px,100%);height:100%;max-height:none;margin:0;border-radius:0;
box-shadow:-30px 0 60px -30px rgba(28,25,23,.5);translate:100% 0}
.mo-panel[open]{translate:0 0}
@starting-style{.mo-panel:modal{translate:100% 0}}
.mo-panel .mo-head{padding-bottom:16px;border-bottom:1px solid var(--mo-line)}
.mo-feed{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:18px 24px 20px}
.mo-feed:focus-visible{outline:2px solid var(--mo-accent);outline-offset:-4px}
.mo-feed .mo-h3:not(:first-child){margin-top:18px}
.mo-events{list-style:none;margin:0;padding:0}
.mo-events li{position:relative;display:flex;align-items:flex-start;gap:12px;padding:10px 0}
.mo-events li:not(:last-child)::before{content:"";position:absolute;top:50px;bottom:-6px;left:17px;width:2px;border-radius:2px;background:var(--mo-line)}
.mo-av{position:relative;flex:none;display:grid;place-items:center;width:36px;height:36px;border-radius:50%;background:#ffedd5;color:#9a3412;font-size:.75rem;font-weight:700}
.mo-av img{width:100%;height:100%;border-radius:50%;object-fit:cover}
.is-new .mo-av::after{content:"";position:absolute;top:-1px;right:-1px;width:11px;height:11px;border-radius:50%;background:#ea580c;box-shadow:0 0 0 2px #fff}
.mo-event{flex:1;min-width:0}
.mo-event p{margin:0;color:var(--mo-muted);font-size:.875rem;line-height:1.45}
.mo-event b,.mo-event strong{color:var(--mo-ink);font-weight:600}
.mo-event time{display:block;margin-top:2px;color:var(--mo-muted);font-size:.78rem}
.mo-event .mo-quote{margin:6px 0 0;padding:7px 12px;border-left:3px solid #fb923c;border-radius:0 8px 8px 0;background:var(--mo-soft);color:var(--mo-ink);font-size:.85rem}
/* 3. Pop-up sheet: a card anchored to the bottom that rises in and drops back out */
.mo-sheet{--mo-dur:.34s;--mo-ease:cubic-bezier(.3,0,.2,1);position:fixed;inset:auto 0 16px;width:min(560px,calc(100% - 24px));height:fit-content;max-height:calc(100dvh - 32px);margin:0 auto;
border-radius:22px;overflow-y:auto;overscroll-behavior:contain;translate:0 calc(100% + 32px)}
.mo-sheet[open]{translate:0 0}
@starting-style{.mo-sheet:modal{translate:0 calc(100% + 32px)}}
.mo-sheet .mo-icon{border-radius:50%}
.mo-choices{margin:0;padding:0;border:1px solid var(--mo-line);border-radius:14px}
.mo-choice{display:flex;align-items:flex-start;gap:16px;padding:14px 16px}
.mo-choice+.mo-choice{border-top:1px solid var(--mo-line)}
.mo-choice-text{flex:1;min-width:0}
.mo-choice label{display:flex;align-items:center;gap:8px;font-weight:600;cursor:pointer}
.mo-choice p{margin:2px 0 0;color:var(--mo-muted);font-size:.85rem;line-height:1.45}
.mo-pill{padding:1px 8px;border-radius:999px;background:#f5f5f4;color:#44403c;font-size:.7rem;font-weight:600}
.mo-switch{flex:none;width:40px;height:24px;margin:2px 0 0;border-radius:999px;appearance:none;cursor:pointer;
background:#78716c radial-gradient(circle,#fff 7.5px,transparent 8px) no-repeat left center/24px 24px;transition:background-color .2s,background-position .2s}
.mo-switch:checked{background-color:var(--mo-accent);background-position:right center}
.mo-switch:disabled{opacity:.55;cursor:not-allowed}
.mo-switch:focus-visible{outline:2px solid var(--mo-accent);outline-offset:2px}
@media (max-width:600px){
.mo-zoom{max-height:calc(100dvh - 32px)}
.mo-head,.mo-body,.mo-foot{padding-left:18px;padding-right:18px}
.mo-feed{padding-left:18px;padding-right:18px}
.mo-invite{grid-template-columns:minmax(0,1fr) auto}
.mo-invite .mo-field:first-child{grid-column:1/-1}
.mo-foot .mo-btn{flex:1 1 auto}
.mo-sheet{inset:auto 0 0;width:100%;max-height:calc(100dvh - 16px);border-radius:22px 22px 0 0}
}
@media (prefers-reduced-motion:reduce){
.mo,.mo::backdrop,.mo-btn,.mo-x,.mo-switch{transition:none}
}
JavaScript
// Everything above works without this script in browsers with invoker commands and closedby.
// It only fills the gaps in older browsers, and does nothing where those features exist.
// Focus goes back to the button that opened a dialog natively when the dialog closes.
if (!('command' in HTMLButtonElement.prototype)) {
// No invoker commands: wire command="show-modal" / command="close" buttons to the dialog API
document.addEventListener('click', function (e) {
var btn = e.target.closest && e.target.closest('button[commandfor]');
var dialog = btn && document.getElementById(btn.getAttribute('commandfor'));
if (!dialog) return;
if (btn.getAttribute('command') === 'show-modal' && !dialog.open) dialog.showModal();
else if (btn.getAttribute('command') === 'close') dialog.close();
});
}
if (!('closedBy' in HTMLDialogElement.prototype)) {
// No closedby: give closedby="any" dialogs their light dismiss (a click outside the box, on the backdrop)
document.querySelectorAll('dialog[closedby="any"]').forEach(function (dialog) {
dialog.addEventListener('click', function (e) {
var r = dialog.getBoundingClientRect();
if (e.target === dialog && (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom)) dialog.close();
});
});
}
Bootstrap 5 code
Requires: Bootstrap 5.3.8 CSS
HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<symbol id="mo-i-invite" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="9.5" cy="8" r="3.5"/><path d="M3.5 19.5c.7-3.4 3-5 6-5s5.3 1.6 6 5M18.5 8v6M15.5 11h6"/></symbol>
<symbol id="mo-i-pulse" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 12h4l2.5-6.5 4 13 2.5-6.5h4"/></symbol>
<symbol id="mo-i-cookie" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20.5 12.5a8.5 8.5 0 1 1-9-9 3 3 0 0 0 3.5 3.5 3 3 0 0 0 3 3 2.5 2.5 0 0 0 2.5 2.5z"/><path d="M8.5 9.5h.01M9.5 15h.01M14.5 14.5h.01" stroke-width="2.6"/></symbol>
</svg>
<div><span class="lbl">Zoom and fade</span>
<div class="d-flex flex-wrap gap-2"><button class="btn mo-btn btn-primary" type="button" commandfor="mo-invite" command="show-modal" aria-haspopup="dialog"><svg aria-hidden="true"><use href="#mo-i-invite"/></svg>Invite people</button></div></div>
<div><span class="lbl">Slide-over panel</span>
<div class="d-flex flex-wrap gap-2"><button class="btn mo-btn" type="button" commandfor="mo-activity" command="show-modal" aria-haspopup="dialog"><svg aria-hidden="true"><use href="#mo-i-pulse"/></svg>Activity</button></div></div>
<div><span class="lbl">Pop-up sheet</span>
<div class="d-flex flex-wrap gap-2"><button class="btn mo-btn" type="button" commandfor="mo-cookies" command="show-modal" aria-haspopup="dialog"><svg aria-hidden="true"><use href="#mo-i-cookie"/></svg>Cookie settings</button></div></div>
<!-- 1. Zoom and fade. closedby="closerequest": Esc closes it, a stray click outside does not, so a half-typed address survives. -->
<dialog class="mo mo-zoom" id="mo-invite" closedby="closerequest" aria-labelledby="mo-invite-title">
<div class="modal-header">
<span class="mo-icon" aria-hidden="true"><svg><use href="#mo-i-invite"/></svg></span>
<div class="flex-grow-1"><h2 class="modal-title" id="mo-invite-title">Invite people to Northfield</h2><p class="mo-sub">They get an email with a link to join the workspace.</p></div>
<button class="btn-close" type="button" commandfor="mo-invite" command="close" aria-label="Close"></button>
</div>
<div class="modal-body">
<form class="row align-items-end mo-invite" method="dialog">
<div class="col-12 col-sm"><label class="form-label" for="mo-invite-email">Email address</label><input class="form-control" id="mo-invite-email" name="email" type="email" placeholder="[email protected]" autocomplete="off" required autofocus></div>
<div class="col col-sm-auto"><label class="form-label" for="mo-invite-role">Role</label><select class="form-select mo-role" id="mo-invite-role" name="role"><option>Viewer</option><option selected>Editor</option><option>Admin</option></select></div>
<div class="col-auto"><button class="btn mo-btn btn-primary" value="invite">Send invite</button></div>
</form>
<h3 class="mo-h3">People with access</h3>
<ul class="list-group list-group-flush mo-people">
<li class="list-group-item d-flex align-items-center"><img class="rounded-circle" src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&crop=faces&w=96&h=96&q=70" width="38" height="38" alt="Hana Kobayashi"><div class="flex-grow-1 overflow-hidden"><b class="d-block fw-semibold">Hana Kobayashi</b><span class="d-block text-truncate text-body-secondary">[email protected]</span></div><span class="badge rounded-pill mo-badge is-owner">Owner</span></li>
<li class="list-group-item d-flex align-items-center"><img class="rounded-circle" src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&crop=faces&w=96&h=96&q=70" width="38" height="38" alt="Theo Marchetti"><div class="flex-grow-1 overflow-hidden"><b class="d-block fw-semibold">Theo Marchetti</b><span class="d-block text-truncate text-body-secondary">[email protected]</span></div><span class="badge rounded-pill mo-badge">Editor</span></li>
<li class="list-group-item d-flex align-items-center"><img class="rounded-circle" src="https://images.unsplash.com/photo-1580489944761-15a19d654956?auto=format&fit=crop&crop=faces&w=96&h=96&q=70" width="38" height="38" alt="Priya Raman"><div class="flex-grow-1 overflow-hidden"><b class="d-block fw-semibold">Priya Raman</b><span class="d-block text-truncate text-body-secondary">[email protected]</span></div><span class="badge rounded-pill mo-badge">Viewer</span></li>
</ul>
</div>
<div class="modal-footer">
<p class="me-auto mb-0 small text-body-secondary">3 of 10 seats used</p>
<button class="btn mo-btn" type="button" commandfor="mo-invite" command="close">Done</button>
</div>
</dialog>
<!-- 2. Slide-over panel. closedby="any": light dismiss by a backdrop click or Esc, nothing to lose. -->
<dialog class="mo mo-panel" id="mo-activity" closedby="any" aria-labelledby="mo-activity-title">
<div class="modal-header">
<div class="flex-grow-1"><h2 class="modal-title" id="mo-activity-title">Activity</h2><p class="mo-sub">Northfield Ceramics · 3 unread</p></div>
<button class="btn-close" type="button" commandfor="mo-activity" command="close" aria-label="Close"></button>
</div>
<div class="modal-body" tabindex="0" role="region" aria-label="Activity feed">
<h3 class="mo-h3">Today</h3>
<ol class="list-unstyled mb-0 mo-events">
<li class="d-flex align-items-start is-new"><span class="mo-av rounded-circle"><img class="rounded-circle" src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&crop=faces&w=96&h=96&q=70" width="36" height="36" alt="Hana Kobayashi"></span><div class="flex-grow-1"><p><span class="visually-hidden">Unread: </span><b>Hana Kobayashi</b> published <strong>Autumn glaze catalogue</strong></p><time datetime="2026-09-25T09:42">9:42</time></div></li>
<li class="d-flex align-items-start is-new"><span class="mo-av rounded-circle"><img class="rounded-circle" src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&crop=faces&w=96&h=96&q=70" width="36" height="36" alt="Theo Marchetti"></span><div class="flex-grow-1"><p><span class="visually-hidden">Unread: </span><b>Theo Marchetti</b> commented on <strong>Kiln schedule, week 40</strong></p><p class="mo-quote">Firing moves to Thursday. The kiln is booked for the tile order on Wednesday.</p><time datetime="2026-09-25T09:15">9:15</time></div></li>
<li class="d-flex align-items-start is-new"><span class="mo-av rounded-circle"><img class="rounded-circle" src="https://images.unsplash.com/photo-1580489944761-15a19d654956?auto=format&fit=crop&crop=faces&w=96&h=96&q=70" width="36" height="36" alt="Priya Raman"></span><div class="flex-grow-1"><p><span class="visually-hidden">Unread: </span><b>Priya Raman</b> uploaded 12 photos to <strong>Stoneware mugs</strong></p><time datetime="2026-09-25T08:58">8:58</time></div></li>
<li class="d-flex align-items-start"><span class="mo-av rounded-circle" aria-hidden="true">JW</span><div class="flex-grow-1"><p><b>Jonas Weber</b> invited [email protected] as a Viewer</p><time datetime="2026-09-25T08:31">8:31</time></div></li>
<li class="d-flex align-items-start"><span class="mo-av rounded-circle"><img class="rounded-circle" src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&crop=faces&w=96&h=96&q=70" width="36" height="36" alt="Hana Kobayashi"></span><div class="flex-grow-1"><p><b>Hana Kobayashi</b> changed the price of <strong>Speckled bowl, large</strong> to £34</p><time datetime="2026-09-25T08:03">8:03</time></div></li>
</ol>
<h3 class="mo-h3">Yesterday</h3>
<ol class="list-unstyled mb-0 mo-events">
<li class="d-flex align-items-start"><span class="mo-av rounded-circle"><img class="rounded-circle" src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&crop=faces&w=96&h=96&q=70" width="36" height="36" alt="Theo Marchetti"></span><div class="flex-grow-1"><p><b>Theo Marchetti</b> completed <strong>Order 1187: pack and ship</strong></p><time datetime="2026-09-24T17:40">17:40</time></div></li>
<li class="d-flex align-items-start"><span class="mo-av rounded-circle" aria-hidden="true">AM</span><div class="flex-grow-1"><p><b>Ama Mensah</b> joined the workspace as an Editor</p><time datetime="2026-09-24T16:12">16:12</time></div></li>
<li class="d-flex align-items-start"><span class="mo-av rounded-circle"><img class="rounded-circle" src="https://images.unsplash.com/photo-1580489944761-15a19d654956?auto=format&fit=crop&crop=faces&w=96&h=96&q=70" width="36" height="36" alt="Priya Raman"></span><div class="flex-grow-1"><p><b>Priya Raman</b> archived <strong>Summer market 2026</strong></p><time datetime="2026-09-24T11:05">11:05</time></div></li>
</ol>
</div>
<form class="modal-footer" method="dialog">
<a class="btn mo-btn" href="#">Full log</a>
<button class="btn mo-btn btn-primary" value="read">Mark all as read</button>
</form>
</dialog>
<!-- 3. Pop-up sheet. closedby="closerequest": no light dismiss, because a click on the page behind is not a consent choice.
Esc still closes it, and saves nothing. -->
<dialog class="mo mo-sheet" id="mo-cookies" closedby="closerequest" aria-labelledby="mo-cookies-title" aria-describedby="mo-cookies-desc">
<form class="d-flex flex-column flex-grow-1 m-0" method="dialog">
<div class="modal-header">
<span class="mo-icon" aria-hidden="true"><svg><use href="#mo-i-cookie"/></svg></span>
<div class="flex-grow-1"><h2 class="modal-title" id="mo-cookies-title">Cookie settings</h2><p class="mo-sub" id="mo-cookies-desc">Choose which cookies Northfield Ceramics may use. You can change this at any time from the link in the footer.</p></div>
</div>
<div class="modal-body">
<fieldset class="mo-choices">
<legend class="visually-hidden">Cookie categories</legend>
<div class="form-check form-switch mo-choice">
<div class="flex-grow-1"><label class="form-check-label" for="mo-ck-necessary">Necessary <span class="badge rounded-pill mo-pill">Always on</span></label><p id="mo-ck-necessary-desc">Keep you signed in, remember your basket and protect the checkout.</p></div>
<input class="form-check-input" type="checkbox" role="switch" id="mo-ck-necessary" aria-describedby="mo-ck-necessary-desc" checked disabled>
</div>
<div class="form-check form-switch mo-choice">
<div class="flex-grow-1"><label class="form-check-label" for="mo-ck-analytics">Analytics</label><p id="mo-ck-analytics-desc">Count visits and see which pages are read, so we can fix what is slow or confusing.</p></div>
<input class="form-check-input" type="checkbox" role="switch" id="mo-ck-analytics" name="analytics" aria-describedby="mo-ck-analytics-desc">
</div>
<div class="form-check form-switch mo-choice">
<div class="flex-grow-1"><label class="form-check-label" for="mo-ck-marketing">Marketing</label><p id="mo-ck-marketing-desc">Show our ads on other sites and measure whether they lead to an order.</p></div>
<input class="form-check-input" type="checkbox" role="switch" id="mo-ck-marketing" name="marketing" aria-describedby="mo-ck-marketing-desc">
</div>
</fieldset>
</div>
<div class="modal-footer">
<button class="btn mo-btn" value="reject">Reject all</button>
<button class="btn mo-btn btn-primary" value="save">Save choices</button>
</div>
</form>
</dialog>
CSS
/* Bootstrap version of the modern-CSS modals. Bootstrap's Modal plugin can't be driven by invoker commands, so this set
uses the same native <dialog> as the plain version (no Bootstrap JavaScript) and borrows Bootstrap's look:
.modal-header/.modal-body/.modal-footer (fed by --bs-modal-* variables), .btn, .btn-close, .form-control, .form-select,
.form-switch, .list-group and .badge. Enter and exit animations are CSS only: @starting-style + allow-discrete on
display and overlay. @starting-style is keyed to :modal, so a static copy with [open] renders in its final state. */
html{scrollbar-gutter:stable} /* the page does not shift sideways when the scroll lock hides its scrollbar */
html:has(.mo:modal){overflow:hidden} /* scroll lock while one of these dialogs is open */
.mo-btn{--bs-btn-padding-x:16px;--bs-btn-padding-y:9px;--bs-btn-font-size:.9rem;--bs-btn-font-weight:600;--bs-btn-line-height:1.2;--bs-btn-border-radius:10px;
--bs-btn-bg:#fff;--bs-btn-color:#1c1917;--bs-btn-border-color:#d6d3d1;--bs-btn-hover-bg:#f5f5f4;--bs-btn-hover-color:#1c1917;--bs-btn-hover-border-color:#a8a29e;
--bs-btn-active-bg:#e7e5e4;--bs-btn-active-color:#1c1917;--bs-btn-active-border-color:#a8a29e;
display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:42px;white-space:nowrap}
.mo-btn.btn-primary{--bs-btn-bg:#c2410c;--bs-btn-color:#fff;--bs-btn-border-color:#c2410c;--bs-btn-hover-bg:#9a3412;--bs-btn-hover-color:#fff;--bs-btn-hover-border-color:#9a3412;
--bs-btn-active-bg:#7c2d12;--bs-btn-active-color:#fff;--bs-btn-active-border-color:#7c2d12;box-shadow:0 6px 16px -8px rgba(234,88,12,.8)}
.mo-btn:focus-visible{outline:2px solid #c2410c;outline-offset:2px;box-shadow:none}
.mo-btn svg{width:18px;height:18px;flex:none}
/* The dialog shell, shared by all three variants. Bootstrap's body and modal variables are scoped to it. */
.mo{--bs-body-color:#1c1917;--bs-secondary-color:#57534e;--bs-border-color:#e7e5e4;
--bs-modal-padding:24px;--bs-modal-header-padding:22px 24px 6px;--bs-modal-header-padding-x:24px;--bs-modal-header-padding-y:22px;--bs-modal-header-border-width:0;
--bs-modal-header-border-color:#e7e5e4;--bs-modal-inner-border-radius:0;--bs-modal-title-line-height:1.35;--bs-modal-footer-gap:10px;--bs-modal-footer-bg:#fafaf9;
--bs-modal-footer-border-color:#e7e5e4;--bs-modal-footer-border-width:1px;--mo-dur:.26s;--mo-ease:cubic-bezier(.2,.8,.2,1);
flex-direction:column;max-width:none;padding:0;border:0;background:#fff;color:#1c1917;font-size:.95rem;line-height:1.55;overflow:hidden;
box-shadow:0 0 0 1px rgba(28,25,23,.06),0 30px 70px -22px rgba(28,25,23,.5);
transition:opacity var(--mo-dur) var(--mo-ease),scale var(--mo-dur) var(--mo-ease),translate var(--mo-dur) var(--mo-ease),
display var(--mo-dur) allow-discrete,overlay var(--mo-dur) allow-discrete}
.mo[open]{display:flex}
.mo::backdrop{background-color:rgb(28 25 23 / 0);backdrop-filter:blur(0);
transition:background-color .26s,backdrop-filter .26s,display .26s allow-discrete,overlay .26s allow-discrete}
.mo[open]::backdrop{background-color:rgb(28 25 23 / .5);backdrop-filter:blur(3px)}
@starting-style{.mo:modal::backdrop{background-color:rgb(28 25 23 / 0);backdrop-filter:blur(0)}}
.mo .modal-header{align-items:flex-start;gap:14px}
.mo .modal-title{font-size:1.15rem;font-weight:700;letter-spacing:-.01em}
.mo-sub{margin:3px 0 0;color:#57534e;font-size:.85rem}
.mo-icon{flex:none;display:grid;place-items:center;width:42px;height:42px;border-radius:12px;background:linear-gradient(135deg,#fff7ed,#ffedd5);color:#c2410c}
.mo-icon svg{width:22px;height:22px}
.mo .modal-header .btn-close{flex:none;width:36px;height:36px;margin:-4px -8px 0 auto;padding:0;border-radius:10px;background-size:13px;opacity:.75}
.mo .btn-close:hover{background-color:#f5f5f4;opacity:1}
.mo .btn-close:focus-visible{outline:2px solid #c2410c;outline-offset:1px;box-shadow:none;opacity:1}
.mo .modal-body{padding:16px 24px 22px}
.mo .modal-footer{gap:10px;padding:14px 24px}
.mo .modal-footer>*{margin:0}
.mo-h3{margin:0 0 6px;line-height:1.55;color:#57534e;font-size:.72rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase}
/* 1. Zoom and fade: scales up from .94 and fades in, and does the reverse on close */
.mo-zoom{position:fixed;inset:0;width:min(500px,calc(100% - 32px));height:fit-content;max-height:calc(100dvh - 48px);margin:auto;border-radius:18px;
overflow-y:auto;overscroll-behavior:contain;opacity:0;scale:.94}
.mo-zoom[open]{opacity:1;scale:1}
@starting-style{.mo-zoom:modal{opacity:0;scale:.94}}
.mo-invite{--bs-gutter-x:10px;--bs-gutter-y:10px;margin-bottom:22px}
@media (min-width:576px){.mo-invite .mo-role{width:118px}}
.mo .form-label{margin-bottom:6px;color:#1c1917;font-size:.8rem;font-weight:600}
.mo .form-control,.mo .form-select{min-height:42px;border-color:#8c857f;border-radius:10px;color:#1c1917;font-size:.9rem}
.mo .form-control::placeholder{color:#78716c}
.mo .form-control:focus,.mo .form-select:focus{border-color:#c2410c;box-shadow:none}
.mo .form-control:focus-visible,.mo .form-select:focus-visible{outline:2px solid #c2410c;outline-offset:1px}
.mo-people{--bs-list-group-bg:transparent;--bs-list-group-color:#1c1917;--bs-list-group-border-color:#e7e5e4;--bs-list-group-item-padding-x:0;--bs-list-group-item-padding-y:10px;
border-top:1px solid #e7e5e4}
.mo-people .list-group-item{gap:12px;font-size:.875rem;line-height:1.35}
.mo-people img{flex:none;object-fit:cover}
.mo-people .text-truncate{font-size:.8rem}
.mo .mo-badge{padding:5px 10px;background:#f5f5f4;color:#44403c;font-size:.75rem;font-weight:600}
.mo .mo-badge.is-owner{background:#ffedd5;color:#9a3412}
/* 2. Slide-over panel: full height from the right, slides in and back out */
.mo-panel{--mo-dur:.34s;--mo-ease:cubic-bezier(.3,0,.2,1);position:fixed;inset:0 0 0 auto;width:min(420px,100%);height:100%;max-height:none;margin:0;border-radius:0;
box-shadow:-30px 0 60px -30px rgba(28,25,23,.5);translate:100% 0}
.mo-panel[open]{translate:0 0}
@starting-style{.mo-panel:modal{translate:100% 0}}
.mo-panel .modal-header{padding-bottom:16px;border-bottom:1px solid #e7e5e4}
.mo-panel .modal-body{min-height:0;overflow-y:auto;overscroll-behavior:contain;padding-top:18px;padding-bottom:20px}
.mo-panel .modal-body:focus-visible{outline:2px solid #c2410c;outline-offset:-4px}
.mo-panel .modal-body .mo-h3:not(:first-child){margin-top:18px}
.mo-events li{position:relative;gap:12px;padding:10px 0}
.mo-events li:not(:last-child)::before{content:"";position:absolute;top:50px;bottom:-6px;left:17px;width:2px;border-radius:2px;background:#e7e5e4}
.mo-av{position:relative;flex:none;display:grid;place-items:center;width:36px;height:36px;background:#ffedd5;color:#9a3412;font-size:.75rem;font-weight:700}
.mo-av img{width:100%;height:100%;object-fit:cover}
.is-new .mo-av::after{content:"";position:absolute;top:-1px;right:-1px;width:11px;height:11px;border-radius:50%;background:#ea580c;box-shadow:0 0 0 2px #fff}
.mo-events p{margin:0;color:#57534e;font-size:.875rem;line-height:1.45}
.mo-events b,.mo-events strong{color:#1c1917;font-weight:600}
.mo-events time{display:block;margin-top:2px;color:#57534e;font-size:.78rem}
.mo-events .mo-quote{margin:6px 0 0;padding:7px 12px;border-left:3px solid #fb923c;border-radius:0 8px 8px 0;background:#fafaf9;color:#1c1917;font-size:.85rem}
/* 3. Pop-up sheet: a card anchored to the bottom that rises in and drops back out */
.mo-sheet{--mo-dur:.34s;--mo-ease:cubic-bezier(.3,0,.2,1);position:fixed;inset:auto 0 16px;width:min(560px,calc(100% - 24px));height:fit-content;max-height:calc(100dvh - 32px);margin:0 auto;
border-radius:22px;overflow-y:auto;overscroll-behavior:contain;translate:0 calc(100% + 32px)}
.mo-sheet[open]{translate:0 0}
@starting-style{.mo-sheet:modal{translate:0 calc(100% + 32px)}}
.mo-sheet .mo-icon{border-radius:50%}
.mo-choices{border:1px solid #e7e5e4;border-radius:14px}
.mo-choice.form-check{display:flex;align-items:flex-start;gap:16px;min-height:0;margin:0;padding:14px 16px}
.mo-choice+.mo-choice{border-top:1px solid #e7e5e4}
.mo-choice .form-check-label{display:flex;align-items:center;gap:8px;color:#1c1917;font-weight:600;cursor:pointer}
.mo-choice p{margin:2px 0 0;color:#57534e;font-size:.85rem;line-height:1.45}
.mo .mo-pill{padding:3px 8px;background:#f5f5f4;color:#44403c;font-size:.7rem;font-weight:600}
.mo-choice .form-check-input{float:none;flex:none;width:40px;height:24px;margin:2px 0 0;border-color:#78716c;background-color:#78716c;cursor:pointer;
--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2.9' fill='%23fff'/%3e%3c/svg%3e");
transition:background-color .2s,background-position .2s}
.mo-choice .form-check-input:checked{border-color:#c2410c;background-color:#c2410c}
.mo-choice .form-check-input:focus{border-color:#78716c;box-shadow:none;--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2.9' fill='%23fff'/%3e%3c/svg%3e")}
.mo-choice .form-check-input:checked:focus{border-color:#c2410c}
.mo-choice .form-check-input:focus-visible{outline:2px solid #c2410c;outline-offset:2px}
.mo-choice .form-check-input:disabled{opacity:.55;cursor:not-allowed}
@media (max-width:600px){
.mo-zoom{max-height:calc(100dvh - 32px)}
.mo{--bs-modal-header-padding:22px 18px 6px}
.mo .modal-body,.mo .modal-footer{padding-left:18px;padding-right:18px}
.mo .modal-footer .mo-btn{flex:1 1 auto}
.mo-sheet{inset:auto 0 0;width:100%;max-height:calc(100dvh - 16px);border-radius:22px 22px 0 0}
}
@media (prefers-reduced-motion:reduce){
.mo,.mo::backdrop,.mo-btn,.mo-choice .form-check-input{transition:none}
}
JavaScript
// Everything above works without this script in browsers with invoker commands and closedby.
// It only fills the gaps in older browsers, and does nothing where those features exist.
// Focus goes back to the button that opened a dialog natively when the dialog closes.
if (!('command' in HTMLButtonElement.prototype)) {
// No invoker commands: wire command="show-modal" / command="close" buttons to the dialog API
document.addEventListener('click', function (e) {
var btn = e.target.closest && e.target.closest('button[commandfor]');
var dialog = btn && document.getElementById(btn.getAttribute('commandfor'));
if (!dialog) return;
if (btn.getAttribute('command') === 'show-modal' && !dialog.open) dialog.showModal();
else if (btn.getAttribute('command') === 'close') dialog.close();
});
}
if (!('closedBy' in HTMLDialogElement.prototype)) {
// No closedby: give closedby="any" dialogs their light dismiss (a click outside the box, on the backdrop)
document.querySelectorAll('dialog[closedby="any"]').forEach(function (dialog) {
dialog.addEventListener('click', function (e) {
var r = dialog.getBoundingClientRect();
if (e.target === dialog && (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom)) dialog.close();
});
});
}
/* Modern CSS modals: native <dialog> opened by invoker commands (commandfor + command="show-modal"), closed by command="close"
or <form method="dialog">, light dismiss via closedby. Enter AND exit animations are utilities: starting:[&:modal]: is
@starting-style (keyed to :modal, so a static copy with [open] renders in its final state) and transition-discrete is
transition-behavior:allow-discrete on display and overlay. Only the page-level rules below cannot be utilities. */
html { scrollbar-gutter: stable; } /* no sideways jump when the scroll lock hides the scrollbar */
html:has(dialog:modal) { overflow: hidden; } /* scroll lock while a modal dialog is open */
JavaScript
// Everything above works without this script in browsers with invoker commands and closedby.
// It only fills the gaps in older browsers, and does nothing where those features exist.
// Focus goes back to the button that opened a dialog natively when the dialog closes.
if (!('command' in HTMLButtonElement.prototype)) {
// No invoker commands: wire command="show-modal" / command="close" buttons to the dialog API
document.addEventListener('click', function (e) {
var btn = e.target.closest && e.target.closest('button[commandfor]');
var dialog = btn && document.getElementById(btn.getAttribute('commandfor'));
if (!dialog) return;
if (btn.getAttribute('command') === 'show-modal' && !dialog.open) dialog.showModal();
else if (btn.getAttribute('command') === 'close') dialog.close();
});
}
if (!('closedBy' in HTMLDialogElement.prototype)) {
// No closedby: give closedby="any" dialogs their light dismiss (a click outside the box, on the backdrop)
document.querySelectorAll('dialog[closedby="any"]').forEach(function (dialog) {
dialog.addEventListener('click', function (e) {
var r = dialog.getBoundingClientRect();
if (e.target === dialog && (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom)) dialog.close();
});
});
}
About this set
Three modal dialogs that open, close and animate with HTML and CSS alone, for a ceramics studio’s workspace. Zoom and fade is an invite dialog that scales up from 94 percent and fades in, and plays the same animation in reverse when it closes; it holds an email field, a role select, a Send invite button and the three people who already have access. Slide-over panel is a full-height activity feed that slides in from the right and back out, with eight updates grouped by day. Pop-up sheet is a cookie settings card that rises from the bottom with three labelled switches and Reject all and Save choices. Each trigger is a button with command=”show-modal” and commandfor, so no script opens anything, and close buttons use command=”close” or a form with method=”dialog”. The activity panel has closedby=”any”, so a click on the backdrop closes it; the invite and cookie dialogs use closedby=”closerequest”, so only Esc or a button closes them and a stray click cannot throw away typed input or a consent choice. @starting-style and transitions with allow-discrete animate the dialog and its ::backdrop both ways, overscroll-behavior keeps scrolling inside the panel, and html:has(dialog:modal) locks the page. A short script fills in invoker commands and closedby only where a browser lacks them.
What’s included
Opened with invoker commands: command="show-modal" and commandfor, no script
Light dismiss with closedby="any", or Esc only with closedby="closerequest"
Enter and exit animations with @starting-style and allow-discrete, backdrop included
overscroll-behavior:contain and a :has() scroll lock with scrollbar-gutter:stable
Fallback script that runs only where invoker commands or closedby are missing
Accessibility
Each dialog is a native modal dialog labelled by its heading, and the cookie sheet is also described by its intro sentence. Focus moves into the dialog, the page behind is inert, Esc closes it and the browser returns focus to the button that opened it. Icon-only buttons have labels, the switches are real checkboxes with role="switch" and linked descriptions, unread activity items say Unread in visually hidden text, and every control shows an orange focus outline. All enter and exit animations are switched off under prefers-reduced-motion.
Customise it
Change the --mo-* custom properties on .mo (accent, ink, muted, line, soft, and --mo-dur and --mo-ease for the animation). The Bootstrap version keeps the same native dialog and borrows .btn, .btn-close, .form-control, .form-select, .form-switch and .list-group, tuned through their --bs-* variables; in Tailwind edit the starting:open: and open: utilities and the #c2410c accent.