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.

Edit profile

Update your profile details below.

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

  • media slot — optional; a cover image flush to the top edge. The close button floats over it in its dark theme.
  • Header — title and subtitle, or the header slot for your own markup (a heading plus supporting text, or a FeaturedIcon row). The icon slot sits above the title block, or beside it with iconPosition="start". Always includes a close button (command="close").
  • Body — default slot; the only region that scrolls.
  • footer slot — optional; a bottom action row, buttons at the end, or two equal columns with footerLayout="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:

Modal

Centered dialog content.

Panel (end)

End-edge drawer (right in LTR).

Panel (start)

Start-edge drawer (left in LTR).

Panel (top)

Top-edge drawer.

Panel (bottom)

Bottom-edge drawer.

Fullscreen

Fills the viewport.
Show 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:

Delete project?

All files and settings will be removed. This cannot be undone.

Invite teammates

They get an email with a link that works for seven days.

Show 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:

AL

Ana Lima

Product designer · Lisbon

Joined in March. Owns the onboarding flow and the design tokens.

Mountain trail

12 km · 640 m elevation

A loop above the tree line with two water stops. Start before nine to beat the heat.

Show 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:

Custom heading

The slot replaces the title block.

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

Activity

Last 30 days

Entry 1: a change was made to the project settings and saved.

Entry 2: a change was made to the project settings and saved.

Entry 3: a change was made to the project settings and saved.

Entry 4: a change was made to the project settings and saved.

Entry 5: a change was made to the project settings and saved.

Entry 6: a change was made to the project settings and saved.

Entry 7: a change was made to the project settings and saved.

Entry 8: a change was made to the project settings and saved.

Entry 9: a change was made to the project settings and saved.

Entry 10: a change was made to the project settings and saved.

Entry 11: a change was made to the project settings and saved.

Entry 12: a change was made to the project settings and saved.

Entry 13: a change was made to the project settings and saved.

Entry 14: a change was made to the project settings and saved.

Entry 15: a change was made to the project settings and saved.

Entry 16: a change was made to the project settings and saved.

Entry 17: a change was made to the project settings and saved.

Entry 18: a change was made to the project settings and saved.

Entry 19: a change was made to the project settings and saved.

Entry 20: a change was made to the project settings and saved.

Entry 21: a change was made to the project settings and saved.

Entry 22: a change was made to the project settings and saved.

Entry 23: a change was made to the project settings and saved.

Entry 24: a change was made to the project settings and saved.

Entry 25: a change was made to the project settings and saved.

Entry 26: a change was made to the project settings and saved.

Entry 27: a change was made to the project settings and saved.

Entry 28: a change was made to the project settings and saved.

Entry 29: a change was made to the project settings and saved.

Entry 30: a change was made to the project settings and saved.

Entry 31: a change was made to the project settings and saved.

Entry 32: a change was made to the project settings and saved.

Entry 33: a change was made to the project settings and saved.

Entry 34: a change was made to the project settings and saved.

Entry 35: a change was made to the project settings and saved.

Entry 36: a change was made to the project settings and saved.

Entry 37: a change was made to the project settings and saved.

Entry 38: a change was made to the project settings and saved.

Entry 39: a change was made to the project settings and saved.

Entry 40: a change was made to the project settings and saved.

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

Share

Anyone with the link can view.

Copy the link below.

Show 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:

Wide

A centred dialog 48rem wide.

Wide drawer

An end drawer 32rem wide.

Short sheet

A bottom sheet capped at 40dvh.

Padding 0

Edge to edge: bring your own padding for media.

Padding 2

Inner padding of 2 spacing steps.

Padding 3

Inner padding of 3 spacing steps.

Padding 4

Inner padding of 4 spacing steps.

Padding 5

Inner padding of 5 spacing steps.

Padding 6

Inner padding of 6 spacing steps.
Show 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

Dimmed

The default scrim; the page behind cannot scroll.

Blurred

The scrim plus a 4px blur.

Transparent

The page behind does not dim and keeps scrolling, but it is inert until this closes.
Show 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.

Discard changes?

You have unsaved edits.

Show 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), respects prefers-reduced-motion.
  • Backdrop — fades to the --overlay scrim on open (::backdrop); blurred adds 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-run init when you update. Add scrollbar-gutter: stable to html if 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.

Publish changes?

Visitors see the new version right away. You can roll back from the history page.

New project

You can change these later.

Show 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 via showModal() (through command="show-modal") — the browser traps focus inside it and returns focus to the invoking button on close, for free.

  • The title renders as an <h2 id="${id}-title">, wired to aria-labelledby on the <dialog> — when title is set, it’s announced as the dialog’s accessible name on open.

  • subtitle renders as a paragraph wired to aria-describedby, so it is read as the dialog’s description.

  • The close button has aria-label="Close". With the header slot the dialog is labelled by the slot’s content; pass aria-labelledby to point at one heading inside it instead.

  • No JS — opening, closing via the buttons, and Escape all still work (native). Only the backdrop-click-to-close fallback is unavailable, and only in browsers without native closedby support.

  • subtitle sets aria-describedby. When you render your own description in the header slot, pass aria-describedby yourself and leave subtitle empty.

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 commandfor to the dialog’s id exactly — a mismatch silently no-ops.
  • Don’t stack multiple modal dialogs — the design isn’t built for it.
  • Don’t use full-screen for 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.