elements
Demos UI Feed Discuss Install
Style System
  • Style
  • Themes
  • Behaviors
Forms
  • Button
  • ButtonGroup
  • Checkbox
  • ColorPicker
  • Combobox
  • Field
  • FileUpload
  • Input
  • InputGroup
  • InputOTP
  • Label
  • NativeSelect
  • NumberInput
  • PasswordInput
  • RadioGroup
  • Select
  • Slider
  • Switch
  • TagsInput
  • Textarea
  • Toggle
  • ToggleGroup
Overlays
  • AlertDialog
  • Command
  • ContextMenu
  • Dialog
  • Drawer
  • DropdownMenu
  • HoverCard
  • Menubar
  • NavigationMenu
  • Popover
  • Sheet
  • Toast
  • Tooltip
Display
  • Accordion
  • Alert
  • AspectRatio
  • Avatar
  • Badge
  • Breadcrumb
  • Card
  • Collapsible
  • Empty
  • Item
  • Kbd
  • Pagination
  • Progress
  • Separator
  • Skeleton
  • Spinner
  • Table
  • Tabs
  • Tree
Layout
  • Carousel
  • Resizable
  • ScrollArea
  • Sidebar
Dates
  • Calendar
  • DatePicker
  • DateRangePicker
  • TimePicker
Data
  • AreaChart
  • BarChart
  • Chart
  • DataTable
  • Kanban
  • LineChart
  • PieChart
  • RadarChart
  • RadialChart
  • VirtualList
Chat
  • Attachment
  • Message
  • MessageList
  • PromptInput
Elements UI / Overlays / Dialog

Dialog

A modal window over the page that traps focus until it closes.

Basic

Uncontrolled: the trigger opens it; Escape, a click outside, the X or Cancel close it.

Code
<Dialog title="Edit profile" description="Make changes to your profile. Save when you're done.">
  <Button slot="trigger">Edit profile</Button>
  <div class="field">
    <label for="name">Name</label>
    <input id="name" value={form.name}>
  </div>
  <DialogFooter>
    <DialogClose><Button>Cancel</Button></DialogClose>
    <Button variant="primary" onclick={() => save(form)}>Save changes</Button>
  </DialogFooter>
</Dialog>

Usage

import { Dialog, DialogFooter, DialogClose } from "@elements/ui/dialog";

<Dialog title="Edit profile" description="Make changes, then save.">
  <Button slot="trigger">Edit profile</Button>
  <input value={form.name}>
  <DialogFooter>
    <DialogClose><Button>Cancel</Button></DialogClose>
    <Button variant="primary" onclick={() => save(form)}>Save</Button>
  </DialogFooter>
</Dialog>

<!-- controlled: open is two-way -->
<Dialog open={state.editing} title="Edit profile">...</Dialog>

Examples

Controlled

Bind open to state. The dialog writes false back when it closes itself; onOpenChange reports its own requests.

open: false changes: reasons:
Code
<Button onclick={() => state.open = true}>Open from anywhere</Button>
<p>open: {state.open}</p>

<Dialog open={state.open} title="Controlled"
        onOpenChange={(open) => log(open)}>
  ...
</Dialog>

Requires a choice

No close button, and neither Escape nor a click outside closes it.

Code
<Dialog title="Accept the terms" closeButton={false}
        dismissOnEscape={false} dismissOnOutside={false}>
  <Button slot="trigger">Review terms</Button>
  ...
  <DialogFooter>
    <DialogClose><Button variant="primary">I accept</Button></DialogClose>
  </DialogFooter>
</Dialog>

Escape guard

onEscapeKeyDown can call preventDefault() to keep the dialog open.

Code
<Dialog title="Draft" onEscapeKeyDown={(e) => state.locked && e.preventDefault()}>
  <label><input type="checkbox" checked={state.locked}> Keep open on Escape</label>
</Dialog>

Escape reaches the page

onEscapeKeyDown can call allowPropagation(): Escape closes the dialog and still reaches the page's own key handlers.

Code
<Dialog title="Shortcuts" onEscapeKeyDown={(e) => e.allowPropagation()}>
  <Button slot="trigger">Shortcuts</Button>
  <p>Escape closes this dialog and still reaches the page's own key handler.</p>
</Dialog>

Focus by interaction type

initialFocus and finalFocus can be functions of how the dialog opened or closed: keyboard, mouse, touch or pen.

Code
<Dialog title="Find a file"
        initialFocus={(type) => type === "keyboard" ? "[data-search]" : true}
        finalFocus={(type) => type === "keyboard" ? true : "[data-after]"}>
  <Button slot="trigger">Find a file</Button>
  <input aria-label="Folder" value="Documents">
  <input aria-label="Search" data-search>
</Dialog>

Initial focus

initialFocus names the element to focus on open; without it, the first field gets focus.

Code
<Dialog title="Rename file" initialFocus="[data-initial]">
  <input value="report.pdf">
  <input value="final-report.pdf" data-initial>
</Dialog>

One dialog in place of another

Add closes the address dialog and opens a confirmation in its place. OK returns focus to the button that opened the first dialog.

Code
<Button onclick={() => state.address = true}>Add delivery address</Button>

<Dialog open={state.address} title="Add delivery address">
  ...fields...
  <DialogFooter>
    <DialogClose><Button>Cancel</Button></DialogClose>
    <Button variant="primary" onclick={() => { state.address = false; state.added = true; }}>Add</Button>
  </DialogFooter>
</Dialog>

<Dialog open={state.added} title="Address added" initialFocus="[data-ok]" closeButton={false}>
  <DialogFooter>
    <DialogClose><Button variant="primary" attrs={{ "data-ok": true }}>OK</Button></DialogClose>
  </DialogFooter>
</Dialog>

Nested

A dialog opened from a dialog. Escape closes the top one; focus returns to the button that opened it.

Code
<Dialog title="Settings">
  <Button slot="trigger">Settings</Button>
  <Dialog title="Delete account?" size="sm">
    <Button slot="trigger" variant="danger">Delete account</Button>
    ...
  </Dialog>
</Dialog>

Title and description parts

DialogTitle and DialogDescription take rich content and are wired to aria-labelledby and aria-describedby.

Code
<Dialog>
  <Button slot="trigger">Release notes</Button>
  <DialogHeader>
    <DialogTitle>Version <em>2.4</em></DialogTitle>
    <DialogDescription>What changed this week.</DialogDescription>
  </DialogHeader>
  ...
</Dialog>

Non-modal

modal={false}: no backdrop, no focus trap, the page stays usable; a click or focus outside closes it.

reasons:
Code
<Dialog title="Inspector" modal={false}>
  <Button slot="trigger">Open inspector</Button>
  ...
</Dialog>

Trap focus only

modal="trap-focus": Tab stays inside, but the page scrolls and takes clicks. onInteractOutside can keep it open.

outside: 0
Code
<Dialog title="Filters" modal="trap-focus"
        onInteractOutside={(e) => state.pinned && e.preventDefault()}>
  <Button slot="trigger">Open filters</Button>
  ...
</Dialog>

Focus on close and focus events

finalFocus names where focus goes on close. onOpenAutoFocus and onCloseAutoFocus can take over focus with preventDefault().

reasons: complete: closeFocus: 0
Code
<Dialog title="Add rows" finalFocus="[data-new-row]">
  <Button slot="trigger">Add rows</Button>
  ...
</Dialog>

<Dialog title="Focus events"
        onOpenAutoFocus={(e) => { e.preventDefault(); save.focus(); }}
        onCloseAutoFocus={(e) => e.preventDefault()}
        onOpenChange={(open, reason) => log(reason)}
        onOpenChangeComplete={(open) => log(open)}>
  ...
</Dialog>

Close after an async submit

A controlled dialog stays open while the form saves, then the page closes it.

Code
<Dialog open={state.renaming} title="Rename project">
  <form onsubmit={(e) => save(state, e)}>
    <input value={state.name}>
    <Button type="submit" variant="primary" loading={state.saving}>Save</Button>
  </form>
</Dialog>

async function save(state, e) {
  e.preventDefault();
  state.saving = true;
  await rename(state.name);
  state.saving = false;
  state.renaming = false;
}

Several triggers

Any number of buttons, anywhere on the page, open one controlled dialog with their own content. triggers names them: the one that opened it is marked open, and focus returns to it.

Code
<Button attrs={{ "data-person": "" }} onclick={() => open(state, "Ada Lovelace")}>Ada Lovelace</Button>
<Button attrs={{ "data-person": "" }} onclick={() => open(state, "Grace Hopper")}>Grace Hopper</Button>

<Dialog open={state.profile} title={state.person} triggers="[data-person]">
  <p>Viewing {state.person}.</p>
</Dialog>

Open from a menu

A menu item opens a controlled dialog. On close, focus goes back to the menu's button, since the item is gone.

Code
<DropdownMenu>
  <Button slot="trigger">Actions</Button>
  <DropdownMenuItem onSelect={() => state.renaming = true}>Rename...</DropdownMenuItem>
</DropdownMenu>

<Dialog open={state.renaming} title="Rename">...</Dialog>

Long content, page scroll

By default a tall dialog scrolls with the overlay, like a page. Focus starts on the text at the top, so it opens at its start.

Code
<Dialog title="Terms of service" initialFocus="[data-start]">
  <Button slot="trigger">Read terms</Button>
  <p tabindex="-1" data-start>These terms cover your use of the service.</p>
  ...many paragraphs...
</Dialog>

Scrollable body, sticky footer

scroll="inside": the panel fits the screen, the header and footer stay put and DialogBody scrolls.

Code
<Dialog title="Terms of service" scroll="inside">
  <Button slot="trigger">Read terms</Button>
  <DialogBody>...many paragraphs...</DialogBody>
  <DialogFooter>...</DialogFooter>
</Dialog>

Elements outside the panel

Content can sit outside the visible panel and stay in the tab order. With no title, label names the dialog.

Code
<Dialog label="Photo" closeButton={false}>
  <Button slot="trigger">View photo</Button>
  <DialogClose>
    <Button icon round label="Close photo" class="outside-close"><Icon name="x" /></Button>
  </DialogClose>
  <img src="./photo.jpg" alt="">
</Dialog>

Right to left

Under dir=rtl the header, close button and footer mirror.

Code
<div dir="rtl">
  <Dialog title="تعديل الملف" closeLabel="إغلاق">
    <Button slot="trigger">فتح الحوار</Button>
    ...
  </Dialog>
</div>

Pointer outside

onPointerDownOutside sees each press outside; preventDefault keeps it open. returnFocus={false} leaves focus where it is on close.

pointer outside: 0
Code
<Dialog title="Pinned note" returnFocus={false}
        onPointerDownOutside={(e) => e.preventDefault()}>
  <Button slot="trigger">Open pinned note</Button>
  <input aria-label="Note">
</Dialog>

Open on load

open={true} without a binding: the dialog is open when the page loads, then owns its state. Escape closes it and the trigger opens it again.

Code
<Dialog open={true} title="What's new">
  <Button slot="trigger">What's new</Button>
  <p>Open when the page loads. The dialog owns open from here.</p>
</Dialog>

Non-modal, stays open

modal={false} with dismissOnOutside={false}: neither a click nor focus outside closes it.

Code
<Dialog title="Inspector" modal={false} dismissOnOutside={false}>
  <Button slot="trigger">Open pinned inspector</Button>
  <input aria-label="Height" value="240">
</Dialog>

Full screen

size="full": a wide panel on a desktop, and edge to edge on a phone.

Code
<Dialog title="Compose" size="full">
  <Button slot="trigger">Compose</Button>
  ...
</Dialog>

Focus fallback

A focus function that returns null falls back to the default: the first field on open, the trigger on close.

Code
<Dialog title="Move file"
        initialFocus={() => document.querySelector<HTMLElement>("[data-recent]")}
        finalFocus={() => document.querySelector<HTMLElement>("[data-recent]")}>
  <Button slot="trigger">Move file</Button>
  <input aria-label="Folder">
</Dialog>

Footer close button

DialogFooter closeButton appends an outline Close button that closes the dialog.

Code
<Dialog title="Share link" description="Anyone with the link can view this file." closeButton={false}>
  <Button slot="trigger">Share</Button>
  <input aria-label="Link" value="https://example.com/f/42" readonly>
  <DialogFooter closeButton/>
</Dialog>

Open without moving focus

initialFocus={() => false}: the dialog opens and focus stays where it was, here on the trigger, which closes it again.

Code
<Dialog title="Find" modal={false} dismissOnOutside={false} initialFocus={() => false}>
  <Button slot="trigger">Find in page</Button>
  <input aria-label="Find">
</Dialog>
Reference Dialog in the manual Every part, attribute, slot, key and data attribute.
Prev ContextMenuNext Drawer
Product
Manual Pricing Compare
Company
History Contact
Legal
Privacy Policy Acceptable Use Terms License Agreement
© Elements Dev Corporation 2026