Modal
Accessible overlay dialog built on native <dialog>.
A modal dialog built on the native `<dialog>` element, so the platform supplies the focus trap, Esc-to-close and an inert backdrop. Bind `open` to your state; the dialog opens/closes in lock-step. Optional `header`/`footer` snippets frame the body, `closeOnBackdrop`/`closeOnEsc` tune dismissal, and `danger` flags destructive dialogs. Sets `aria-labelledby` to the `title` automatically, or accepts an `aria-label` when untitled.
Demo
Usage
<script>
import { Modal, Button } from 'dssoca';
let open = $state(false);
</script>
<Button onclick={() => (open = true)}>Open</Button>
<Modal bind:open title="Confirm action">
{#snippet footer()}
<Button variant="ghost" onclick={() => (open = false)}>Cancel</Button>
<Button onclick={() => (open = false)}>Confirm</Button>
{/snippet}
<p>Are you sure you want to continue?</p>
</Modal>
<!-- Fills the viewport: header/footer pinned, body scrolls, size axis inert -->
<Modal bind:open={logsOpen} fullscreen title="Log stream">
<LogStream {lines} />
</Modal>Uses the native `<dialog>` element: the browser provides the focus trap, Esc handling and an inert `::backdrop`. `aria-labelledby` points at the title when set; otherwise pass `aria-label`. The close button carries `aria-label="Close"`. Size controls the dialog max-width only. Zero border-radius. With `fullscreen` the dialog fills the viewport (`100dvh`, `vh` fallback) and the size axis has no effect on width; because the panel is full-bleed there is no backdrop area left to hit, so `closeOnBackdrop` is unreachable in that mode — the close button and Esc are the exits. Responsiveness stays with the caller: pass `fullscreen={width < 640}` to mirror MUI's full-screen-on-mobile pattern.
HTML
interactive with vanilla.js The exact markup the component renders, generated at build time. Use it with dssoca/vanilla.css+ dssoca/vanilla.js — see Plain HTML & CSS.
<button class="ss-btn secondary" type="button" data-ss-modal="#confirm">open</button>
<dialog id="confirm" class="ss-modal" aria-labelledby="modal-s1-title">
<div class="panel">
<header class="head">
<h2 class="title" id="modal-s1-title">Delete item</h2>
<button type="button" class="close" data-ss-dismiss aria-label="Close">×</button>
</header>
<div class="body">
<p>This cannot be undone.</p>
</div>
<footer class="foot">
<button class="ss-btn secondary" type="button" data-ss-dismiss>cancel</button>
<button class="ss-btn danger" type="button" data-ss-dismiss>delete</button>
</footer>
</div>
</dialog>A native <dialog>. vanilla.js opens it from any [data-ss-modal="#id"] element and closes it from [data-ss-dismiss] or a backdrop click (add data-ss-static to keep it open; data-ss-no-esc blocks Escape).
Props
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | false | Bindable. Whether the modal is shown; syncs to the native dialog. |
title | string | — | Header title; wires `aria-labelledby`. Omit and pass `aria-label` for an untitled dialog. |
closeOnBackdrop | boolean | true | Clicking outside the panel (on the backdrop) closes the modal. |
closeOnEsc | boolean | true | Esc closes the modal. When false the cancel event is prevented. |
danger | boolean | false | Styling hint for destructive dialogs (tints the title). |
fullscreen | boolean | false | Fill the viewport instead of a centred, capped panel: header and footer stay pinned, the body scrolls, the size axis goes inert and the backdrop is no longer clickable. |
aria-label | string | — | Accessible name used when no `title` is set. |
onclose | () => void | — | Called after the dialog closes (Esc, backdrop, or button). |
header | Snippet | — | Replaces the default title row in the header. |
footer | Snippet | — | Footer content, typically a trailing cancel→confirm action pair (the affirmative `Button` trails the dismissive `ghost` one, per dialog convention). |
children | Snippet | — | Required. The modal body content. |
size | 'sm' | 'md' | 'lg' | — | Per-instance size override; inherits the ancestor `data-size-variant` when unset. |