Example Components
Modal Dialog
A button that opens a native modal <dialog>. The browser handles focus trapping, Escape, the top layer, and returning focus to the button; the component adds a close button, closing on a backdrop click, and a scroll lock.
Usage
trigger is the button text and title is the dialog heading, which is also its accessible name. The default slot is the dialog body.
true
examples/components/modal-dialog/modal-dialog.html
html
<modal-dialog data-bascik-prop-trigger="Delete project" data-bascik-prop-title="Delete this project?">
<p>This removes every page and component. It cannot be undone.</p>
</modal-dialog> html examples/components/modal-dialog/modal-dialog.html
<!-- @bascik
A button that opens a native modal <dialog>. Escape, the close button, and a click on the backdrop close it, and the browser returns focus to the button.
@prop trigger - Text of the button that opens the dialog.
@prop title - Dialog heading, also its accessible name.
@slot default - Dialog body content.
-->
<style>
.trigger {
padding: 0.5rem 1rem;
border: 1px solid var(--color-border, color-mix(in srgb, currentColor 30%, transparent));
border-radius: var(--radius, 0.5rem);
background: var(--color-surface, color-mix(in srgb, currentColor 8%, transparent));
color: inherit;
font: inherit;
cursor: pointer;
}
.trigger:hover {
border-color: var(--color-accent, currentColor);
}
.dialog {
width: min(32rem, calc(100vw - 2rem));
padding: 0;
border: 1px solid var(--color-border, color-mix(in srgb, currentColor 30%, transparent));
border-radius: var(--radius, 0.5rem);
background: var(--color-bg, Canvas);
color: var(--color-text, CanvasText);
box-shadow: 0 1rem 3rem rgb(0 0 0 / 0.35);
}
.dialog::backdrop {
background: rgb(0 0 0 / 0.5);
}
/* Lock page scrolling while this dialog is open. `html` is never renamed, the class is. */
html:has(.dialog[open]) {
overflow: hidden;
}
.content {
padding: 1.25rem;
}
.header {
display: flex;
align-items: start;
justify-content: space-between;
gap: 1rem;
}
.title {
margin: 0;
font-size: 1.25rem;
}
.close {
border: 0;
background: none;
color: inherit;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
}
</style>
<button class="trigger" id="trigger" type="button" aria-haspopup="dialog" data-bascik-prop-trigger>Open</button>
<dialog class="dialog" id="dialog" aria-labelledby="title">
<div class="content">
<div class="header">
<h2 class="title" id="title" data-bascik-prop-title>Dialog</h2>
<button class="close" id="close" type="button" aria-label="Close">×</button>
</div>
<div data-bascik-slot></div>
</div>
</dialog>
<script>
const dialog = document.getElementById('dialog');
document.getElementById('trigger').addEventListener('click', () => dialog.showModal());
document.getElementById('close').addEventListener('click', () => dialog.close());
// The dialog has no padding, so a click whose target is the dialog itself landed on the backdrop.
dialog.addEventListener('click', (event) => {
if (event.target === dialog) dialog.close();
});
</script> Output is shown unminified (HTML, CSS, JS, and identifier names) for readability.
How It Works
showModal()puts the dialog in the top layer, makes the rest of the page inert, and moves focus inside. Closing returns focus to the button that opened it.- A click whose target is the
<dialog>element itself landed on the backdrop, because the dialog has no padding of its own, so the script closes it. html:has(.dialog[open]) { overflow: hidden; }stops the page scrolling behind the dialog. Bascik never renameshtml, and.dialogis scoped, so the rule only matches this component's dialogs.aria-labelledbyon the dialog points at its heading. Bascik rewrites both IDs per instance.
Adapt It
- For a confirmation, put a
<form method="dialog">with two buttons inside the dialog and readdialog.returnValuein acloselistener. - To open the dialog without a script, use an invoker command:
<button commandfor="dialog" command="show-modal">. Check its browser support first; it reached Baseline later than<dialog>.