Skip to content

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

ApproachWhen to use
One island owns everythingopen, 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.StaticSSR / 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

PropTypeDefaultDescription
triggerunknownCustom trigger markup.
childrenunknownComposed dialog slots, or trigger content when no Dialog.Content child is present.
contentunknownDialog panel content.
size"sm" | "base" | "lg" | "xl""base"Dialog width.
role"dialog" | "alertdialog""dialog"Dialog semantic role.
defaultOpenbooleanfalseInitial open state.
closeOnClickOutsidebooleantrue (false for alert dialogs)Close when clicking outside content.
closeOnEscapebooleantrueClose when pressing Escape.
lockScrollbooleantrueLock body scroll while open.
onOpenChange(open: boolean) => voidCalled when open state changes.
onPortalMounted(container: HTMLElement) => voidAfter 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