SvModal
An accessible modal dialog: a backdrop, a focus trap, Escape and backdrop close, and a scale-in animation.
SvModal centers a portalled dialog over everything, locks page scroll, and
traps focus inside for as long as it is open - all through the shared a11y
primitives, so its behaviour matches SvDrawer and nested overlays
close top-first. It can optionally be dragged by its header and resized from the
bottom-right handle. Controlled via a bindable open.
Basic usage
<script lang="ts">
import { SvModal, SvButton } from '@svgrid/grid'
let open = $state(false)
</script>
<SvButton onclick={() => (open = true)}>Edit row</SvButton>
<SvModal bind:open title="Edit row">
<p>Body content.</p>
{#snippet footer()}<SvButton variant="primary" onclick={() => (open = false)}>Save</SvButton>{/snippet}
</SvModal>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
open |
boolean |
false |
Controlled, bindable open state. |
onClose |
() => void |
- | Called whenever the dialog closes. |
title |
string |
- | Header heading; also names the dialog for a11y. |
size |
sm | md | lg |
md |
Preset dialog width (360 / 520 / 720px). |
draggable |
boolean |
false |
Allow moving the dialog by its header. |
resizable |
boolean |
false |
Show a bottom-right handle to resize the dialog. |
closeOnBackdrop |
boolean |
true |
Close when the backdrop is clicked. |
closeOnEsc |
boolean |
true |
Close on the Escape key. |
hideClose |
boolean |
false |
Hide the header close (x) button. |
children |
Snippet |
- | Dialog body. |
footer |
Snippet |
- | Footer content, typically the action buttons. |
Patterns
Draggable and resizable
Opt into a movable, resizable window for tools users want to keep open while they work elsewhere in the page:
<SvModal bind:open title="Inspector" draggable resizable size="lg">
<p>Drag by the header, resize from the corner.</p>
</SvModal>
Confirmations
For a decision the user must make, drop the dismiss escapes and the close button so the only way out is a footer action:
<SvModal bind:open title="Delete 3 rows?" size="sm"
closeOnBackdrop={false} closeOnEsc={false} hideClose>
<p>This cannot be undone.</p>
{#snippet footer()}
<SvButton onclick={() => (open = false)}>Cancel</SvButton>
<SvButton variant="danger" onclick={confirm}>Delete</SvButton>
{/snippet}
</SvModal>
Edit a grid row
The workhorse pattern: open on a row action, edit a copy of the record, commit
in the footer, and use onClose to discard the draft however the dialog leaves
(Save, Cancel, Escape, or backdrop):
<script lang="ts">
import { SvModal, SvButton, SvTextInput } from '@svgrid/grid'
let open = $state(false)
let draft = $state({ name: '', email: '' })
function editRow(row) { draft = { ...row }; open = true }
function save() { /* ...persist draft */ open = false }
function reset() { draft = { name: '', email: '' } }
</script>
<SvModal bind:open title="Edit contact" size="md" onClose={reset}>
<SvTextInput label="Name" bind:value={draft.name} />
<SvTextInput label="Email" bind:value={draft.email} />
{#snippet footer()}
<SvButton onclick={() => (open = false)}>Cancel</SvButton>
<SvButton variant="primary" onclick={save}>Save</SvButton>
{/snippet}
</SvModal>
Tip: focus is trapped inside the dialog while it is open, so Tab cycles through the form fields and buttons without escaping to the page behind. The first focusable element receives focus on open, so lead with the field the user edits.
Accessibility
- The dialog is
role="dialog"witharia-modal="true", andtitlesupplies itsaria-labelledby. - Focus is trapped inside while open and page scroll is locked; both release on close through the shared primitives.
- Escape and backdrop clicks are honoured per
closeOnEsc/closeOnBackdropand routed through the dismissable layer stack, so a nested overlay closes first.
See also
- Overlays overview - the whole floating-surface family.
- SvDrawer - the same dialog contract as an edge side-sheet.
- SvPopover - a lightweight, non-modal anchored panel.