Skip to content
Areia
Esc
navigateopen⌘Jpreview
On this page

Dialog

Displays modal content over the page.

Source Code

import { ilha } from "ilha";
import { Button, Dialog } from "areia";

export default ilha.render(() => (
  <Dialog
    trigger={<Button>Open dialog</Button>}
    contentClass="grid gap-4 p-6"
    content={
      <>
        <div class="grid gap-2">
          <Dialog.Title>Edit profile</Dialog.Title>
          <Dialog.Description>
            Make changes to your profile. Close the dialog when
            you are done.
          </Dialog.Description>
        </div>
        <div class="flex justify-end gap-2">
          <Dialog.Close>
            <Button variant="secondary">Cancel</Button>
          </Dialog.Close>
          <Dialog.Close>
            <Button variant="primary">Save</Button>
          </Dialog.Close>
        </div>
      </>
    }
  />
));

Import

import { Dialog } from "areia";

Usage

Use Dialog for an interactive dialog. The composition-first API accepts Dialog.Trigger, Dialog.Portal, Dialog.Overlay, and Dialog.Content as children. The trigger and content props remain available as shortcuts.

import { ilha } from "ilha";
import { Button, Dialog } from "areia";

export default ilha.render(() => (
  <Dialog>
    <Dialog.Trigger>
      <Button>Open</Button>
    </Dialog.Trigger>
    <Dialog.Portal>
      <Dialog.Overlay />
      <Dialog.Content class="grid gap-4 p-6">
        <Dialog.Title>Dialog title</Dialog.Title>
        <Dialog.Description>
          Helpful supporting text for the dialog.
        </Dialog.Description>
        <Dialog.Close>
          <Button>Close</Button>
        </Dialog.Close>
      </Dialog.Content>
    </Dialog.Portal>
  </Dialog>
));

Examples

Ilha + portaled overlays (dialog, popover, dropdown, hover-card)

@areia/slots moves portal wrappers (dialog-portal, popover/dropdown positioners, etc.) to document.body when overlays open. That subtree is outside the Ilha island host element. The same constraints apply to Popover, Dropdown, and HoverCard when portal is enabled (default).

Ilha’s renderer morphs only the island host (data-ilha). After the portal is teleported, reactive updates from the owning island (bound inputs, each(results), nested data-ilha-slot children inside dialog-content) may not reach portaled nodes. You can see a frozen snapshot (empty search field, command-list still hidden, or an unhydrated nested island placeholder).

Supported patterns today

Approach When to use
One island owns everything open, query, and list markup live in a single parent .render. Prefer the shortcut content={…} or composed Dialog.Content without a separate child island only inside the portal.
Imperative bridge (escape hatch) onPortalMounted(container) on portaled primitives — Dialog, Popover, Dropdown, HoverCard, Tooltip, ContextMenu, Combobox (slots), Select (slots). Use to sync portaled DOM (e.g. command palette bridge). Remove when Ilha can patch teleported subtrees.
Dialog.Static SSR / docs only — not for signal-driven UI inside a portaled panel.

Close controls: Dialog.Close and Popover.Close with as="button" (default) wrap icon-only children (SVG / Icon) in <button type="button" data-slot="…-close"> instead of putting the close slot on the SVG alone.

Alert dialog

Use role: "alertdialog" for confirmation flows that require explicit acknowledgment. Alert dialogs do not close on outside click by default.

import { ilha } from "ilha";
import { Button, Dialog } from "areia";

export default ilha.render(() => (
  <Dialog
    role="alertdialog"
    trigger={
      <Button variant="destructive">Delete project</Button>
    }
    contentClass="grid gap-4 p-6"
    content={
      <>
        <div class="grid gap-2">
          <Dialog.Title>Delete project?</Dialog.Title>
          <Dialog.Description>
            This action cannot be undone. The project and all
            related data will be permanently deleted.
          </Dialog.Description>
        </div>
        <div class="flex justify-end gap-2">
          <Dialog.Close>
            <Button variant="secondary">Cancel</Button>
          </Dialog.Close>
          <Dialog.Close>
            <Button variant="destructive">Delete</Button>
          </Dialog.Close>
        </div>
      </>
    }
  />
));

Sizes

import { ilha } from "ilha";
import { Button, Dialog } from "areia";

const sizes = ["sm", "base", "lg", "xl"] as const;

export default ilha.render(() => (
  <div class="flex flex-wrap gap-2">
    {sizes.map((size) => (
      <Dialog
        size={size}
        trigger={<Button variant="secondary">{size}</Button>}
        contentClass="grid gap-4 p-6"
        content={
          <>
            <Dialog.Title>{size} dialog</Dialog.Title>
            <Dialog.Description>
              Dialogs constrain their minimum width by size.
            </Dialog.Description>
            <Dialog.Close>
              <Button>Close</Button>
            </Dialog.Close>
          </>
        }
      />
    ))}
  </div>
));

Close behavior

Configure outside-click, Escape, initial open state, and scroll locking through root options.

import { ilha } from "ilha";
import { Button, Dialog } from "areia";

export default ilha.render(() => (
  <Dialog
    closeOnClickOutside={false}
    closeOnEscape
    lockScroll
    trigger={<Button>Open locked dialog</Button>}
    contentClass="grid gap-4 p-6"
    content={
      <>
        <Dialog.Title>Explicit close</Dialog.Title>
        <Dialog.Description>
          Clicking outside this dialog will not close it.
        </Dialog.Description>
        <Dialog.Close>
          <Button>Close</Button>
        </Dialog.Close>
      </>
    }
  />
));

Prop shortcut

For simple dialogs, pass trigger and content. Areia generates the trigger, portal, overlay, and content slots for you. With Ilha, keep reactive content (or composed Dialog.Content children) in the island .render callback so lists and inputs update when signals change; Dialog.Static pre-renders markup for SSR-only shortcuts.

import { ilha } from "ilha";
import { Button, Dialog } from "areia";

export default ilha.render(() => (
  <Dialog
    trigger={<Button>Open shortcut dialog</Button>}
    contentClass="grid gap-4 p-6"
    content={
      <>
        <Dialog.Title>Shortcut API</Dialog.Title>
        <Dialog.Description>
          Use props when you do not need to customize the slot
          structure.
        </Dialog.Description>
        <Dialog.Close>
          <Button>Done</Button>
        </Dialog.Close>
      </>
    }
  />
));

API Reference

Dialog

Prop Type Default Description
trigger unknown Custom trigger markup.
children unknown Composed dialog slots, or trigger content when no Dialog.Content child is present.
content unknown Dialog panel content.
size "sm" | "base" | "lg" | "xl" "base" Dialog width.
role "dialog" | "alertdialog" "dialog" Dialog semantic role.
defaultOpen boolean false Initial open state.
closeOnClickOutside boolean true (false for alert dialogs) Close when clicking outside content.
closeOnEscape boolean true Close when pressing Escape.
lockScroll boolean true Lock body scroll while open.
onOpenChange (open: boolean) => void Called when open state changes.
onPortalMounted (container: HTMLElement) => void After portal moves to document.body on open. Also on Popover, Dropdown, HoverCard; AlertDialog via slots createAlertDialog.

Compound exports

  • Dialog.Trigger
  • Dialog.Portal
  • Dialog.Overlay
  • Dialog.Content
  • Dialog.Title
  • Dialog.Description
  • Dialog.Close

Was this page helpful?