---
title: Dialog
description: Present a labeled modal task with structured content and dismissal.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# Dialog

> Present a labeled modal task with structured content and dismissal.

Use Dialog when an action or form needs focused attention with a labeled modal surface. The trigger and content share disclosure state through the root.

## Basic usage

```tsx
import { Button, Dialog } from 'moraine'

export function Example() {
  return (
    <Dialog>
      <Dialog.Trigger as={Button}>Open</Dialog.Trigger>
      <Dialog.Content title="Settings">
        <Dialog.Body>Preferences</Dialog.Body>
        <Dialog.Footer>
          <Dialog.Close as={Button}>Done</Dialog.Close>
        </Dialog.Footer>
      </Dialog.Content>
    </Dialog>
  )
}
```

## Anatomy

```text
Dialog [component; no DOM]
├── Dialog.Trigger [part; slot=trigger]
├── Dialog.Close [part]
├── overlay [slot]
└── Dialog.Content [part; slot=content]
    ├── Dialog.Header [part; slot=header]
    │   ├── Dialog.Title [part; slot=title]
    │   ├── Dialog.Description [part; slot=description]
    │   └── Dialog.Action [part; slot=action]
    ├── contentClose [slot]
    ├── Dialog.Body [part; slot=body]
    └── Dialog.Footer [part; slot=footer]
```

Content is portaled. Overlay and Content are siblings by default; scrollable overlay mode nests Content inside Overlay.

## Usage

### Content composition

Use Content `title` and `description` for simple cases:

```tsx
<Dialog>
  <Dialog.Content title="Settings" description="Update your account.">
    <Dialog.Body>...</Dialog.Body>
    <Dialog.Footer>...</Dialog.Footer>
  </Dialog.Content>
</Dialog>
```

For a custom header layout, compose the anatomy parts:

```tsx
<Dialog.Content>
  <Dialog.Header>
    <Dialog.Title>Settings</Dialog.Title>
    <Dialog.Description>Update your account.</Dialog.Description>
    <Dialog.Action>...</Dialog.Action>
  </Dialog.Header>
  <Dialog.Body>...</Dialog.Body>
  <Dialog.Footer>...</Dialog.Footer>
</Dialog.Content>
```

An explicit `Dialog.Header` replaces the header generated by Content's `title` and `description`.
`Dialog.Title` and `Dialog.Description` connect their IDs to the surface's accessible name and description.
Without a visible title, supply root `ariaLabel` or native ARIA labeling on Content.

`Dialog.Action` places actions beside the title. `Dialog.Close` is an explicit close control you can
place inside the root; it is separate from Content's automatic corner close button.

### Focus and outside interaction

`modal` defaults to `true`: focus stays inside the surface, outside content is hidden from assistive
technology, and native outside pointer actions are prevented. Closing restores focus to the trigger.

Setting `modal={false}` allows focus to leave the surface, but does not disable Escape or outside
dismissal. `dismissible` controls those close requests. Scroll locking is independent:
`preventScroll` defaults to `true`. For interaction with the background page, also set
`preventScroll={false}` and `overlay={false}`.

### Styling and portal placement

Apply `class` and object `style` directly to each rendered part. Root `classes` and `styles` set
slot defaults for the family; Content's slot maps cover only `overlay`, `content`, and `contentClose`.
See [Customization](https://moraine.subf.dev/docs/customization.md#the-4-layer-override-hierarchy) for precedence.

Set root `portalMount` to place Content in a chosen container. This also works for controlled
surfaces without a Trigger.

### Nested overlays

Nested roots keep dismissal and focus restoration local to the active layer. Escape closes the topmost dialog or popover, then returns focus to its trigger inside the parent dialog.

```tsx
import { Button, Dialog, Popover } from 'moraine'

export function NestedOverlays() {
  return (
    <Dialog>
      <Dialog.Trigger as={Button} variant="outline">
        Open nested overlays
      </Dialog.Trigger>
      <Dialog.Content
        title="Workspace settings"
        description="Review help before confirming your workspace changes."
      >
        <Dialog.Footer class="flex-wrap gap-3">
          <Popover>
            <Popover.Trigger as={Button} variant="outline">
              View settings help
            </Popover.Trigger>
            <Popover.Content ariaLabel="Settings help" class="p-3">
              Changes apply to this workspace only.
            </Popover.Content>
          </Popover>
          <Dialog>
            <Dialog.Trigger as={Button}>Confirm workspace changes</Dialog.Trigger>
            <Dialog.Content
              title="Confirm changes"
              description="Escape closes this confirmation before closing workspace settings."
            >
              <Dialog.Footer>
                <Dialog.Close
                  as={Button}
                  variant="outline"
                  class="px-3 py-1.5 h-auto w-auto static"
                >
                  Return to settings
                </Dialog.Close>
              </Dialog.Footer>
            </Dialog.Content>
          </Dialog>
        </Dialog.Footer>
      </Dialog.Content>
    </Dialog>
  )
}
```

### State and dismissal

Use controlled `open` with `onOpenChange` when the surrounding flow owns visibility. `dismissible={false}` blocks outside and Escape dismissal and calls `onClosePrevent`; provide a clear explicit close path in that case.

```tsx
import { Badge, Button, Dialog } from 'moraine'
import { createSignal, Show } from 'solid-js'

export function StateDismissal() {
  const [open, setOpen] = createSignal(false)
  const [dismissible, setDismissible] = createSignal(false)
  const [preventedAttempts, setPreventedAttempts] = createSignal(0)

  return (
    <div class="flex flex-wrap gap-4 items-center">
      <Button onClick={() => setOpen(true)}>
        Open {dismissible() ? 'Dismissible' : 'Non-Dismissible'} Dialog
      </Button>

      <Button
        variant="outline"
        onClick={() => {
          setDismissible((prev) => !prev)
          setPreventedAttempts(0)
        }}
      >
        Mode: {dismissible() ? 'Dismissible' : 'Strict (Buttons Only)'}
      </Button>

      <Dialog
        open={open()}
        onOpenChange={setOpen}
        dismissible={dismissible()}
        onClosePrevent={() => setPreventedAttempts((c) => c + 1)}
      >
        <Dialog.Content
          title="Unsaved Configuration Changes"
          description="Explicit confirmation is required before navigating away."
        >
          <Dialog.Body>
            <div class="py-2 space-y-3">
              <p class="text-sm text-muted-foreground">
                {dismissible()
                  ? 'Press Escape or click the backdrop to dismiss.'
                  : 'Clicking outside or pressing Escape is blocked. Use the action buttons below.'}
              </p>
              <Show when={preventedAttempts() > 0}>
                <Badge variant="outline" class="text-destructive border-destructive">
                  Blocked {preventedAttempts()} outside dismissal attempt(s)
                </Badge>
              </Show>
            </div>
          </Dialog.Body>
          <Dialog.Footer>
            <div class="flex gap-2 w-full justify-end">
              <Button
                variant="outline"
                onClick={() => {
                  setOpen(false)
                  setPreventedAttempts(0)
                }}
              >
                Discard Changes
              </Button>
              <Button
                onClick={() => {
                  setOpen(false)
                  setPreventedAttempts(0)
                }}
              >
                Save & Apply
              </Button>
            </div>
          </Dialog.Footer>
        </Dialog.Content>
      </Dialog>
    </div>
  )
}
```

### Structure and lifecycle

Use root `scrollable` for long content or `fullscreen` for a full-viewport task. `onExitComplete`
runs after the overlay and content finish their exit animations.

```tsx
import { Badge, Button, Dialog } from 'moraine'
import { createSignal } from 'solid-js'

export function StructureLifecycle() {
  const [exits, setExits] = createSignal(0)
  const [status, setStatus] = createSignal('Idle')

  return (
    <div class="flex flex-wrap gap-4 items-center">
      <Dialog
        onExitComplete={() => {
          setExits((c) => c + 1)
          setStatus('Exit animation completed')
        }}
      >
        <Dialog.Trigger as={Button}>Open Provisioning Dialog</Dialog.Trigger>
        <Dialog.Content
          title="Provision Production Database"
          description="Configure clustering, replication nodes, and automated backup schedules."
        >
          <Dialog.Body>
            <div class="text-muted-foreground leading-relaxed py-2 space-y-2">
              <p>
                Provisioning will allocate dedicated compute instances and initialize encryption
                keys.
              </p>
            </div>
          </Dialog.Body>
          <Dialog.Footer>
            <div class="flex gap-2 w-full justify-end">
              <Button variant="outline">Cancel</Button>
              <Button>Provision Cluster</Button>
            </div>
          </Dialog.Footer>
        </Dialog.Content>
      </Dialog>

      <div class="text-xs text-muted-foreground flex gap-2 items-center">
        <span>Lifecycle status:</span>
        <Badge variant="outline">{status()}</Badge>
        <span>(Completed exits: {exits()})</span>
      </div>
    </div>
  )
}
```

### Keyboard interaction

| Key                    | Description                                                        |
| ---------------------- | ------------------------------------------------------------------ |
| <kbd>Escape</kbd>      | Requests dismissal when `dismissible={true}`.                      |
| <kbd>Tab</kbd>         | Moves focus forward through focusable controls inside the dialog.  |
| <kbd>Shift + Tab</kbd> | Moves focus backward through focusable controls inside the dialog. |

## Examples

### Destructive confirmation

Keep the item visible until the user confirms the action. Cancel and backdrop dismissal leave it intact.

```tsx
import { Button, Dialog } from 'moraine'
import { createSignal, Show } from 'solid-js'

export function DeleteConfirmation() {
  const [exists, setExists] = createSignal(true)
  const [open, setOpen] = createSignal(false)

  return (
    <div class="flex gap-3 items-center">
      <Show when={exists()} fallback={<p class="text-sm">Saved filter removed.</p>}>
        <span class="text-sm">Saved filter: Assigned to me</span>
        <Dialog open={open()} onOpenChange={setOpen}>
          <Dialog.Trigger as={Button} variant="outline" size="sm">
            Delete
          </Dialog.Trigger>
          <Dialog.Content
            title="Delete saved filter?"
            description="Assigned to me will be removed from your saved filters."
          >
            <Dialog.Body>
              <p class="text-sm">You can create another filter later.</p>
            </Dialog.Body>
            <Dialog.Footer>
              <div class="flex gap-2 w-full justify-end">
                <Dialog.Close as={Button} variant="outline">
                  Cancel
                </Dialog.Close>
                <Button
                  variant="destructive"
                  onClick={() => {
                    setExists(false)
                    setOpen(false)
                  }}
                >
                  Delete filter
                </Button>
              </div>
            </Dialog.Footer>
          </Dialog.Content>
        </Dialog>
      </Show>
    </div>
  )
}
```

### Scrollable and fullscreen content

```tsx
import { Button, Dialog } from 'moraine'
import { For } from 'solid-js'

export function ScrollableFullscreen() {
  const SCROLLABLE_LINES = Array.from(
    { length: 100 },
    (_, index) => `Release note line ${index + 1}`,
  )

  return (
    <div class="flex flex-wrap gap-3 items-center">
      <Dialog scrollable>
        <Dialog.Trigger as={Button} variant="secondary">
          Overlay scroll dialog
        </Dialog.Trigger>
        <Dialog.Content title="Release Notes" description="Long content scrolls with the overlay.">
          <Dialog.Body>
            <div class="space-y-1">
              <For each={SCROLLABLE_LINES}>
                {(line) => <p class="text-sm text-foreground">{line}</p>}
              </For>
            </div>
          </Dialog.Body>
        </Dialog.Content>
      </Dialog>
      <Dialog fullscreen>
        <Dialog.Trigger as={Button} variant="secondary">
          Full screen dialog
        </Dialog.Trigger>
        <Dialog.Content title="Release Notes" description="Full screen dialog content.">
          <Dialog.Body>
            <div class="space-y-1">
              <For each={SCROLLABLE_LINES}>
                {(line) => <p class="text-sm text-foreground">{line}</p>}
              </For>
            </div>
          </Dialog.Body>
        </Dialog.Content>
      </Dialog>
    </div>
  )
}
```

### Controlled lifecycle

```tsx
import { Button, Dialog } from 'moraine'
import { createSignal } from 'solid-js'

export function ControlledLifecycle() {
  const [open, setOpen] = createSignal(false)
  const [exitCount, setExitCount] = createSignal(0)

  return (
    <div class="flex gap-3 items-center">
      <Button variant="outline" onClick={() => setOpen(true)}>
        Open dialog
      </Button>
      <p class="text-sm text-muted-foreground">Completed exits: {exitCount()}</p>
      <Dialog
        open={open()}
        onOpenChange={setOpen}
        onExitComplete={() => setExitCount((count) => count + 1)}
      >
        <Dialog.Content title="Controlled dialog">
          <Dialog.Body>
            <p class="text-sm">The parent owns visibility and observes completed exit motion.</p>
          </Dialog.Body>
          <Dialog.Footer>
            <Button onClick={() => setOpen(false)}>Close</Button>
          </Dialog.Footer>
        </Dialog.Content>
      </Dialog>
    </div>
  )
}
```

## Attributes

| Attributes | Slot | Description |
| --- | --- | --- |
| `data-closed` | `dialog-trigger`, `dialog-content`, `dialog-overlay` | Present when disclosure or transition content is closed. |
| `data-disabled` | `dialog-trigger` | Present when the component, slot, or item is disabled. |
| `data-expanded` | `dialog-trigger`, `dialog-content`, `dialog-overlay` | Present when the panel, accordion, or menu is expanded. |
| `data-overlay-scroll` | `dialog-overlay` | Present when scrolling is owned by the overlay. |
| `data-footer` | `dialog-body` | Present when the component renders footer content. |
| `data-header` | `dialog-body` | Present when the component renders header content. |
| `data-scroll` | `dialog-body` | Present when the content region owns scrolling. |

## Props

### Dialog

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| ariaLabel | string \| undefined | — | Accessible name used when the dialog has no rendered title. |
| close | boolean \| undefined | true | Whether to show a close button. |
| closeIcon | IconT.Name \| JSX.Element \| undefined | 'icon-close' | Icon name or custom content for the close button. |
| defaultOpen | boolean \| undefined | false | Initial open state when uncontrolled. |
| dismissible | boolean \| undefined | true | Whether outside interaction and Escape should dismiss the shell. |
| fullscreen | boolean \| null \| undefined | false | Whether the dialog should take up the full viewport. |
| id | string \| undefined | — | Unique identifier used to derive the content id. |
| modal | boolean \| undefined | true | Whether the surface contains focus, restores focus on close, hides outside content from<br>assistive technology, and prevents the native default action of outside pointer events.<br>Outside pointer and Escape dismissal remain controlled by `dismissible`. |
| onClosePrevent | (() => void) \| undefined | — | Called when a dismissal attempt is blocked. |
| onExitComplete | (() => void) \| undefined | — | Called after the modal has fully finished its exit motion. |
| onOpenChange | ((open: boolean) => void) \| undefined | — | Called whenever the open state changes. |
| open | boolean \| undefined | — | Controlled open state. |
| overlay | boolean \| undefined | true | Whether to render the overlay element. |
| portalMount | Node \| undefined | — | Default destination for Modal.Portal; defaults to the trigger document body. |
| preventScroll | boolean \| undefined | true | Whether body scroll should be locked while the shell is present. |
| scrollable | boolean \| null \| undefined | false | Whether the overlay should scroll the complete dialog panel. |
| children | JSX.Element \| undefined | — | Composed trigger and content primitives. |
| classes | Classes \| undefined | — | Family slot class defaults for this Dialog instance. |
| styles | Styles \| undefined | — | Family slot style defaults for this Dialog instance. |

### Dialog.Trigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| disabled | boolean \| undefined | — | Whether this trigger is disabled. |
| as | T \| undefined | 'button' | Element or component to render as. |
| children | JSX.Element \| undefined | — | Trigger label and visual content. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |

### Dialog.Content

Props for Dialog.Content.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| description | JSX.Element \| undefined | — | Secondary description displayed below the title. |
| title | JSX.Element \| undefined | — | Primary title displayed in the dialog header. |
| children | JSX.Element \| undefined | — | Composable dialog parts. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| classes | ContentClasses \| undefined | — | Family slot class defaults for this instance. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
| styles | ContentStyles \| undefined | — | Family slot style defaults for this instance. |

### Dialog.Header

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| as | T \| undefined | 'div' | — |
| children | JSX.Element \| undefined | — | — |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |

### Dialog.Title

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| id | string \| undefined | — | — |
| as | T \| undefined | 'h2' | — |
| children | JSX.Element \| undefined | — | — |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |

### Dialog.Description

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| id | string \| undefined | — | — |
| as | T \| undefined | 'p' | — |
| children | JSX.Element \| undefined | — | — |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |

### Dialog.Action

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| as | T \| undefined | 'div' | — |
| children | JSX.Element \| undefined | — | — |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |

### Dialog.Body

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| as | T \| undefined | 'div' | — |
| children | JSX.Element \| undefined | — | — |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |

### Dialog.Footer

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| as | T \| undefined | 'div' | — |
| children | JSX.Element \| undefined | — | — |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |

### Dialog.Close

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| disabled | boolean \| undefined | — | Whether this trigger is disabled. |
| as | T \| undefined | 'button' | Element or component to render as. |
| children | JSX.Element \| undefined | — | Trigger label and visual content. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
