Skip to content
omaris

Containment

Dialog

MD3 dialog, on the native <dialog> element.

import { Dialog } from 'omaris'
Learn

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.

Invite a teammate

They will get an email with a join link.

Anyone you invite can see every project in this workspace.

<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.

size = sm

Below 640px every size becomes a bottom sheet instead, unless you turn responsive off.

<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.

Step 1 of 3

Line 1 of this step's content.

Line 2 of this step's content.

<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.

You have unsaved changes

Leaving now discards them.

<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: true paints 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 title as the question ("Delete this project?") and description as the consequence. Actions answer it: "Delete" and "Cancel", never "Yes", "No" or "OK".
  • Keep actions to two: the safe one variant="text" first, the committing one filled last.
  • Leave responsive on. Below 640px the dialog becomes a bottom sheet with a drag handle.
  • Use dialog.confirm() before writing a Dialog with bind:open. The component is for a body of your own.

Don't

  • Open a dialog from a dialog. Change the content and let morph grow 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 md dialog. Use size="lg", full, or a page.
  • Show one on page load, or for anything that is not the person's decision.

Quick reference

size
  • sm
  • md (default)
  • lg
  • xl
  • full
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-hidden is 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 bindable

Defaults to false

boolean

Bindable.

size

Defaults to 'md'

DialogSize
sm
md
lg
xl
full
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.