# Dialog

> A native <dialog> opened and closed with invoker commands — modal, edge panel, or fullscreen.

URL: https://apogee.avaui.dev/docs/dialog
Last updated: 2026-10-15

A native <dialog> opened and closed with invoker commands — modal, edge panel, or fullscreen.

Last updated October 15, 2026

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.

```astro
<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

```sh
npx @ava-ui/apogee add dialog
```

Depends on `CloseButton` and `utils` (`responsive.ts`). Pair with `Button` (`command` / `commandfor`) to open and close it.

## Usage

```astro
---
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`:

```astro
<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:

```astro
<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:

```astro
<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:

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

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

```astro
<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:

```astro
<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

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

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

```astro
<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:

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

## Related

[

ToastA transient notification, fired imperatively from anywhere on the page.

](https://apogee.avaui.dev/docs/toast)[

CommandPaletteA ⌘K / Ctrl+K searchable command menu in a native dialog.

](https://apogee.avaui.dev/docs/command-palette)[

PopoverRich content on click, built on the native Popover API — top layer and light dismiss for free, anchored with CSS anchor positioning.

](https://apogee.avaui.dev/docs/popover)
