Dialog
A native <dialog> opened and closed with invoker commands — modal, edge panel, or fullscreen.
Last updated
Uses JavaScript
A small script closes the dialog on backdrop click, as a fallback for browsers
without native closedby support.
Without JS: Opening, closing via the ✕ button, and Escape all still work — they're native dialog + Invoker Commands behaviour, not authored JS. Only the backdrop-click-to-close fallback is lost, and only in browsers that don't yet support the native closedby attribute.
Show codeHide code
<Button command="show-modal" commandfor="demo-dialog">Open dialog</Button>
<Dialog id="demo-dialog" title="Edit profile">
<p>Update your profile details below.</p>
<Button variant="ghost" color="neutral" command="close" commandfor="demo-dialog" slot="footer">Cancel</Button>
<Button command="close" commandfor="demo-dialog" slot="footer">Save</Button>
</Dialog>Add
npx @ava-ui/apogee add dialog
Depends on CloseButton and utils (responsive.ts). Pair with Button
(command / commandfor) to open and close it.
Usage
---
import Dialog from '@/components/apogee/Dialog/Dialog.astro'
import Button from '@/components/apogee/Button/Button.astro'
---
<Button command="show-modal" commandfor="d">Open</Button>
<Dialog id="d" title="Confirm">
<p>Are you sure?</p>
</Dialog>
Anatomy
The dialog is a flex column: the media, header and footer keep their place and only the body scrolls, so a tall drawer never scrolls its buttons away.
mediaslot — optional; a cover image flush to the top edge. The close button floats over it in its dark theme.- Header —
titleandsubtitle, or theheaderslot for your own markup (a heading plus supporting text, or a FeaturedIcon row). Theiconslot sits above the title block, or beside it withiconPosition="start". Always includes a close button (command="close"). - Body — default slot; the only region that scrolls.
footerslot — optional; a bottom action row, buttons at the end, or two equal columns withfooterLayout="fill".
API
@composable — exposed to Content Editors in AVA. Any other attribute (e.g.
aria-describedby) passes through to the root <dialog>.
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string |
— | Required — matches the trigger’s commandfor. |
title |
string |
— | Renders as the header <h2>, wired with aria-labelledby. |
subtitle |
string |
— | One line under the title, wired with aria-describedby. |
position |
Responsive<'center' | 'start' | 'end' | 'top' | 'bottom' | 'full-screen'> |
'center' |
Where it sits, per breakpoint: { base: 'bottom', md: 'center' } is a sheet on phones and a dialog from md. start / end are logical (end is the right in LTR, the left in RTL). |
size |
string |
32rem centre, 24rem drawers, 85dvh sheets |
CSS length: the width of a centred dialog or drawer, the max height of a top or bottom sheet. Use this rather than an inline style, which replaces it. |
padding |
0 | 2 | 3 | 4 | 5 | 6 |
5 |
Inner padding in spacing steps. 0 for edge-to-edge media. |
overlay |
'dim' | 'blurred' | 'transparent' |
'dim' |
Backdrop treatment. transparent leaves page scrolling alone; the page is still inert while the dialog is open. |
dismissOnBackdrop |
boolean |
true |
Clicking the backdrop closes the dialog. Set false for a decision the user must answer. |
iconPosition |
'top' | 'start' |
'top' |
Where the icon slot sits: above the title block, or beside it. |
footerLayout |
'end' | 'fill' |
'end' |
Footer buttons at the end, or stacked on phones and two equal columns from sm. |
class |
string |
— | Extra classes, merged last. |
Variants
Every position:
Show codeHide code
<Button command="show-modal" commandfor="v-modal">Modal</Button>
<Dialog id="v-modal" title="Modal">Centered dialog content.</Dialog>
<Button command="show-modal" commandfor="v-panel-end">Panel — end</Button>
<Dialog id="v-panel-end" position="end" title="Panel (end)">End-edge drawer (right in LTR).</Dialog>
<Button command="show-modal" commandfor="v-panel-start">Panel — start</Button>
<Dialog id="v-panel-start" position="start" title="Panel (start)">Start-edge drawer (left in LTR).</Dialog>
<Button command="show-modal" commandfor="v-panel-top">Panel — top</Button>
<Dialog id="v-panel-top" position="top" title="Panel (top)">Top-edge drawer.</Dialog>
<Button command="show-modal" commandfor="v-panel-bottom">Panel — bottom</Button>
<Dialog id="v-panel-bottom" position="bottom" title="Panel (bottom)">Bottom-edge drawer.</Dialog>
<Button command="show-modal" commandfor="v-full">Fullscreen</Button>
<Dialog id="v-full" position="full-screen" title="Fullscreen">Fills the viewport.</Dialog>Icon, header and media
An icon above or beside the title, and a fill footer:
Show codeHide code
<Dialog id="d" title="Delete project?" subtitle="This cannot be undone." footerLayout="fill">
<FeaturedIcon slot="icon" theme="outline" color="critical"><TriangleAlert /></FeaturedIcon>
…
<Button slot="footer" variant="outline" color="neutral" command="close" commandfor="d">Cancel</Button>
<Button slot="footer" color="critical" command="close" commandfor="d">Delete</Button>
</Dialog>
<Dialog id="d2" title="Invite teammates" subtitle="Send invites by email." iconPosition="start">
<FeaturedIcon slot="icon" color="primary"><UserPlus /></FeaturedIcon>
…
</Dialog>Your own header, and a cover image with the dark close button over it:
Show codeHide code
<Dialog id="d" aria-labelledby="d-h">
<div slot="header" class="flex items-center gap-3">
<Avatar name="Ana Lima" size="large" status="online" />
<div><h2 id="d-h" class="font-semibold">Ana Lima</h2><p class="text-sm text-neutral-faded">Product designer</p></div>
</div>
…
</Dialog>
<Dialog id="d2" title="Mountain trail" padding={4}>
<img slot="media" src="…" alt="" class="h-40" />
…
</Dialog>With both title and a header slot, the slot wins and labels the dialog:
Show codeHide code
<Dialog id="d" title="Fallback" aria-labelledby="d-h">
<h2 slot="header" id="d-h">Custom heading</h2>
</Dialog>A long drawer: the header and footer stay pinned, the body scrolls.
Show codeHide code
<Dialog id="d" position="end" title="Activity">
…forty paragraphs…
<Button slot="footer" command="close" commandfor="d">Done</Button>
</Dialog>Responsive position
Sheet on phones, centred dialog from md. Resize the window and reopen.
Show codeHide code
<Button command="show-modal" commandfor="d-resp">Open</Button>
<Dialog id="d-resp" position={{ base: 'bottom', md: 'center' }} title="Share" subtitle="Anyone with the link can view.">
<p>Copy the link below.</p>
</Dialog>A position object needs a base key. Without one, the dialog falls back to the browser’s centred layout below the first breakpoint you set.
Size and padding
size is a width for a centred dialog or drawer and a max height for a sheet.
Every padding step:
Show codeHide code
<Dialog id="d-wide" size="48rem" title="Wide">…</Dialog>
<Dialog id="d-drawer" position="end" size="32rem" title="Wide drawer">…</Dialog>
<Dialog id="d-sheet" position="bottom" size="40dvh" title="Short sheet">…</Dialog>
<Dialog id="d-pad-0" padding={0} title="Padding 0">…</Dialog>
<Dialog id="d-pad-2" padding={2} title="Padding 2">…</Dialog>
… padding 3, 4, 5, 6 …Overlay
Show codeHide code
<Dialog id="d-ov-dim" overlay="dim" title="Dimmed">…</Dialog>
<Dialog id="d-ov-blur" overlay="blurred" title="Blurred">…</Dialog>
<Dialog id="d-ov-clear" overlay="transparent" title="Transparent">…</Dialog>Must answer
dismissOnBackdrop={false}: the backdrop click does nothing; Escape, the close
button and the footer buttons still work.
Show codeHide code
<Dialog id="d-stay" dismissOnBackdrop={false} title="Discard changes?">
<p>You have unsaved edits.</p>
<Button variant="ghost" color="neutral" command="close" commandfor="d-stay" slot="footer">Keep editing</Button>
<Button color="critical" command="close" commandfor="d-stay" slot="footer">Discard</Button>
</Dialog>States
- Open / closed — the native
[open]attribute; enter/exit fades and slides (@starting-style), respectsprefers-reduced-motion. - Backdrop — fades to the
--overlayscrim on open (::backdrop);blurredadds a 4px blur. Page scrolling is locked while a dim or blurred dialog is open: the rule lives in the tokens file (body:has(dialog:modal)), so re-runinitwhen you update. Addscrollbar-gutter: stabletohtmlif the layout shift bothers you.
Try each state via the triggers in the Variants preview above.
Examples
Five bodies cover most dialogs. A stacked confirm (icon above, 25rem, fill
footer), a horizontal one (icon beside, buttons at the end), a destructive
confirm, a media dialog and a form.
Show codeHide code
<Dialog id="confirm" title="Publish changes?" footerLayout="fill" size="25rem">
<FeaturedIcon slot="icon" color="positive"><CircleCheck /></FeaturedIcon>
<p>Visitors see the new version right away.</p>
<Button slot="footer" variant="outline" color="neutral" command="close" commandfor="confirm">Cancel</Button>
<Button slot="footer" command="close" commandfor="confirm">Publish</Button>
</Dialog>
<Dialog id="form" title="New project" subtitle="You can change these later.">
<form class="flex flex-col gap-4"><Input label="Name" /><Textarea label="Description" rows={3} /></form>
<Button slot="footer" variant="ghost" color="neutral" command="close" commandfor="form">Cancel</Button>
<Button slot="footer" type="submit" command="close" commandfor="form">Create</Button>
</Dialog>A destructive confirm, with a critical footer button:
<Button color="critical" command="show-modal" commandfor="confirm-delete">Delete account</Button>
<Dialog id="confirm-delete" title="Delete account" subtitle="This can't be undone." dismissOnBackdrop={false}>
<FeaturedIcon slot="icon" theme="outline" color="critical"><TriangleAlert /></FeaturedIcon>
<Button variant="ghost" color="neutral" command="close" commandfor="confirm-delete" slot="footer">Cancel</Button>
<Button color="critical" command="close" commandfor="confirm-delete" slot="footer">Delete</Button>
</Dialog>
Accessibility
| Key | Action |
|---|---|
Tab |
Moves between controls inside the open dialog. |
Enter Space |
Activate the focused close or footer button. |
Esc |
Closes the dialog. |
-
Native
<dialog>opened viashowModal()(throughcommand="show-modal") — the browser traps focus inside it and returns focus to the invoking button on close, for free. -
The
titlerenders as an<h2 id="${id}-title">, wired toaria-labelledbyon the<dialog>— whentitleis set, it’s announced as the dialog’s accessible name on open. -
subtitlerenders as a paragraph wired toaria-describedby, so it is read as the dialog’s description. -
The close button has
aria-label="Close". With theheaderslot the dialog is labelled by the slot’s content; passaria-labelledbyto point at one heading inside it instead. -
No JS — opening, closing via the buttons, and
Escapeall still work (native). Only the backdrop-click-to-close fallback is unavailable, and only in browsers without nativeclosedbysupport. -
subtitlesetsaria-describedby. When you render your own description in theheaderslot, passaria-describedbyyourself and leavesubtitleempty.
Tokens
| Token | Used for |
|---|---|
--border-neutral |
Dialog border (centred position) and the footer rule |
--overlay |
Backdrop scrim |
--background-elevation-raised / --foreground-neutral |
Background / default text color |
--foreground-neutral |
Title text |
--foreground-neutral-faded / --background-neutral-faded |
Close button icon / hover background |
--border-primary |
Close button focus ring |
--radius-lg |
Corner radius |
--duration-slow / --ease-standard |
Open/close and backdrop transition |
Do / Don’t
- Do give every dialog a clear
title— it’s the only heading users get. - Do match
commandforto the dialog’sidexactly — a mismatch silently no-ops. - Don’t stack multiple modal dialogs — the design isn’t built for it.
- Don’t use
full-screenfor something that isn’t itself a full task (a short confirm) — reserve it for immersive flows.
Content
- Title is a short noun phrase naming the task (“Edit profile”, “Delete account”), sentence case, no ending punctuation.
- Footer buttons: the affirmative/primary action trailing, a neutral “Cancel” leading.