Containment
Dialog
MD3 dialog, on the native <dialog> element.
import { Dialog } from 'omaris' Examples
Basic
bind:open is the whole API, for a dialog with a body of your own — a short form finished or cancelled in one go. Escape and the backdrop close it.
<script lang="ts"> import { Button, Dialog } from 'omaris'; let open = $state(false);</script><Button onclick={() => (open = true)}>Invite a teammate</Button><Dialog bind:open title="Invite a teammate" description="They will get an email with a join link."> <p class="text-body-md text-muted-foreground"> Anyone you invite can see every project in this workspace. </p> {#snippet actions()} <Button variant="text" onclick={() => (open = false)}>Cancel</Button> <Button onclick={() => (open = false)}>Send invite</Button> {/snippet}</Dialog> Promise dialogs
dialog.confirm() returns a promise, so a confirmation reads as one line instead of a state variable and two callbacks. OmarisProvider renders the host these appear in.
<script lang="ts"> import { Button, dialog, toast } from 'omaris'; async function remove() { const ok = await dialog.confirm({ title: 'Delete this project?', description: 'Everything in it goes too. This cannot be undone.', confirmText: 'Delete', destructive: true }); if (ok) toast.success('Project deleted'); } async function rename() { const name = await dialog.prompt({ title: 'Rename project', value: 'storefront' }); if (name) toast('Renamed to ' + name); }</script><Button variant="tonal" tone="destructive" onclick={remove}>Delete…</Button><Button variant="tonal" onclick={rename}>Rename…</Button> Sizes
Five widths. full fills the screen, which is right for an editor.
<script lang="ts"> import { Button, Dialog } from 'omaris'; let size = $state<'sm' | 'md' | 'lg' | 'xl' | 'full'>('sm'); let open = $state(false); function show(next: typeof size) { size = next; open = true; }</script><Button variant="outlined" size="xs" onclick={() => show('sm')}>sm</Button><Button variant="outlined" size="xs" onclick={() => show('md')}>md</Button><Button variant="outlined" size="xs" onclick={() => show('lg')}>lg</Button><Button variant="outlined" size="xs" onclick={() => show('xl')}>xl</Button><Button variant="outlined" size="xs" onclick={() => show('full')}>full</Button><Dialog bind:open {size} title="size = {size}"> <p class="text-body-md text-muted-foreground"> Below 640px every size becomes a bottom sheet instead, unless you turn <code>responsive</code> off. </p></Dialog> Morphing
The panel resizes itself as its content changes rather than jumping, which is what makes a multi-step dialog feel like one surface.
<script lang="ts"> import { Button, Dialog } from 'omaris'; let open = $state(false); let step = $state(1);</script><Button onclick={() => { step = 1; open = true; }}> Open a three-step dialog</Button><Dialog bind:open title="Step {step} of 3"> <div class="flex flex-col gap-3"> {#each { length: step * 2 }, i (i)} <p class="text-body-md text-muted-foreground">Line {i + 1} of this step's content.</p> {/each} </div> {#snippet actions()} <Button variant="text" disabled={step === 1} onclick={() => step--}>Back</Button> {#if step < 3} <Button onclick={() => step++}>Next</Button> {:else} <Button onclick={() => (open = false)}>Done</Button> {/if} {/snippet}</Dialog> Right to left
Nothing in the dialog is positioned in left/right terms, so dir="rtl" mirrors the whole thing: the headline, the close button in the top corner, and the actions along the bottom.
<script lang="ts"> import { Button, Dialog } from 'omaris'; let open = $state(false);</script><div dir="rtl"> <Button onclick={() => (open = true)}>دعوة زميل</Button></div><Dialog bind:open dir="rtl" title="دعوة زميل إلى مساحة العمل" description="سيصلهم بريد فيه رابط للانضمام."> <p class="text-body-md text-muted-foreground">يمكن لمن تدعوه رؤية كل المشاريع هنا.</p> {#snippet actions()} <Button variant="text" onclick={() => (open = false)}>إلغاء</Button> <Button onclick={() => (open = false)}>إرسال الدعوة</Button> {/snippet}</Dialog> Overridden
closable={false} and dismissible={false} make a dialog you have to answer — use it sparingly, and only when leaving would lose work.
<script lang="ts"> import { Button, Dialog } from 'omaris'; let open = $state(false);</script><Button variant="outlined" onclick={() => (open = true)}>Unsaved changes</Button><Dialog bind:open title="You have unsaved changes" closable={false} dismissible={false} responsive={false} classes={{ panel: 'border-2 border-warning' }}> <p class="text-body-md text-muted-foreground">Leaving now discards them.</p> {#snippet actions()} <Button variant="text" tone="destructive" onclick={() => (open = false)}>Discard</Button> <Button onclick={() => (open = false)}>Keep editing</Button> {/snippet}</Dialog> When to use it
Use it for
- An interruption that needs an answer first: delete, discard, sign out.
dialog.confirm()from the provider is one line and returns a promise;destructive: truepaints the confirming button. - A short form finished or abandoned in one go, like an invite or a rename.
dialog.prompt()gives a single field. - Something the person must read before going on, like an expired session or a failed payment.
dialog.alert(). - A bigger task that still ends in finish or cancel, like an editor or a crop.
size="full". Steps inside it morph the one panel instead of opening a second dialog.
Not for
- A task that keeps the page in view: filters, editing a record beside its table, an inspector → Sheet.
- Feedback that needs no answer, like "Saved" or "Sent" → Toast. A toast never blocks; a dialog always does.
- A few actions off a button → Menu.
- Explaining the screen → Tour.
- A picture full-screen → Image Viewer.
Do
- Phrase
titleas the question ("Delete this project?") anddescriptionas the consequence. Actions answer it: "Delete" and "Cancel", never "Yes", "No" or "OK". - Keep
actionsto two: the safe onevariant="text"first, the committing onefilledlast. - Leave
responsiveon. Below 640px the dialog becomes a bottom sheet with a drag handle. - Use
dialog.confirm()before writing aDialogwithbind:open. The component is for a body of your own.
Don't
- Open a dialog from a dialog. Change the content and let
morphgrow the panel. - Set
dismissible={false}unless leaving would lose work, and then still offer a way out among the actions. - Put a long document in a
mddialog. Usesize="lg",full, or a page. - Show one on page load, or for anything that is not the person's decision.
Quick reference
size smmd(default)lgxlfull
mode dialog(default)sheet
sheet is the same dialog re-anchored to the bottom edge for a phone: full width, one pair of rounded corners, and the sheet keyframes instead of the centered ones.
API
Dialog
MD3 dialog, on the native <dialog> element.
showModal() brings the focus trap, the inert background, the top layer and Escape-to-close for free — all the parts a hand-rolled modal gets subtly wrong. What's added on top is the MD3 look, an exit animation (native close() is instant, so the close is deferred until the animation has run), and the icon/headline/supporting-text layout.
Three things make it feel finished rather than merely correct:
- It resizes smoothly. The panel's height is driven from the measured body, so swapping a step, revealing an error or loading a list grows the dialog with an emphasized curve instead of jumping. Width follows size the same way. - It closes itself. A close button sits at the top end corner — out of the headline's way, and mirrored under dir="rtl" — unless the dialog is explicitly undismissable. - It is a bottom sheet on a phone. Below mobileQuery the same dialog anchors to the bottom edge, grows a drag handle and can be thrown away with a flick — the shape a modal is supposed to take on a touch screen.
import { Dialog } from 'omaris' Parts
Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.
root- No description in the source yet.
panel- The morphing box. Its height is written from the measured body, so it is the one element that knows how tall the dialog should be — and
overflow-hiddenis what lets it lag behind the content during the transition instead of clipping nothing at all. body- Scroll container, and the box whose height the panel follows.
close- Pinned to the panel, so it stays put while the body scrolls.
handle- MD3's drag handle, and the mouse's grab point on a bottom sheet.
grip- No description in the source yet.
icon- No description in the source yet.
header- Headline and supporting text, kept together as one block.
title- No description in the source yet.
description- No description in the source yet.
content- No description in the source yet.
actions- No description in the source yet.
Props
open bindableDefaults to false
boolean Bindable.
size Defaults to 'md'
DialogSize smmdlgxlfull- Edge to edge — the MD3 full-screen dialog.
title string Headline.
description string Supporting text under the headline.
centered Defaults to false
boolean Center the icon, headline and supporting text.
dismissible Defaults to true
boolean Allow Escape and a backdrop press to close it.
closable boolean Close button at the top end corner. Defaults to dismissible.
closeLabel Defaults to 'Close'
string Accessible name for that button.
responsive Defaults to true
boolean Become a bottom sheet on a phone. On by default.
mobileQuery Defaults to '(max-width: 639px)'
string What counts as a phone.
morph Defaults to true
boolean Animate height and width changes. On by default.
drag Defaults to true
boolean Drag the bottom sheet away with a finger. On by default.
onclose () => void Called after the dialog has finished closing.
class string classes DialogClasses Per-part Tailwind overrides. class still covers the root.
icon Snippet Icon above the headline.
children Snippet The body.
actions Snippet Buttons along the bottom.