# `PUI.Dialog`

A modal dialog component for LiveView applications.

## Basic Usage

The simplest way to use a dialog is with a trigger button:

    <.dialog id="my-dialog" title="Dialog title">
      <:trigger :let={attr}>
        <.button {attr}>Open Dialog</.button>
      </:trigger>
      <p>Dialog content goes here.</p>
      <:footer :let={%{hide: hide}}>
        <div class="flex justify-end gap-2">
          <.button variant="outline" phx-click={hide}>Cancel</.button>
          <.button>Confirm</.button>
        </div>
      </:footer>
    </.dialog>

## Accessing Hide/Show Actions

Use `:let` to access the `hide` and `show` JS commands:

    <.dialog :let={%{hide: hide, show: show}} id="my-dialog" title="Project details">
      <:trigger :let={attr}>
        <.button {attr}>Open</.button>
      </:trigger>
      <p>Content</p>
      <:footer>
        <div class="flex justify-end gap-2">
          <.button variant="outline" phx-click={hide}>Close</.button>
          <.button phx-click={show}>Refresh focus</.button>
        </div>
      </:footer>
    </.dialog>

## Server-Controlled Dialog

Control dialog visibility from your LiveView using the `show` attribute:

    # In your LiveView
    def mount(_params, _session, socket) do
      {:ok, assign(socket, show_dialog: false)}
    end

    def handle_event("open", _, socket), do: {:noreply, assign(socket, show_dialog: true)}
    def handle_event("close", _, socket), do: {:noreply, assign(socket, show_dialog: false)}

    # In template - use on_cancel to sync state when dismissed via backdrop/escape
    <.button phx-click="open">Open</.button>
    <.dialog id="my-dialog" show={@show_dialog} on_cancel={JS.push("close")}>
      <p>Server-controlled content</p>
      <.button phx-click="close">Close</.button>
    </.dialog>

### show={} vs :if={}

Two approaches for server-controlled dialogs:

| Approach | Behavior |
|----------|----------|
| `show={@visible}` | Dialog stays in DOM, visibility toggled. Form state preserved, animations work. |
| `:if={@visible}` | Dialog mounted/unmounted. Form state resets, no exit animations. |

**Using `show={}`** (recommended for most cases):

    <.dialog id="dialog" show={@show_dialog} on_cancel={JS.push("close")}>
      ...
    </.dialog>

**Using `:if={}`** (when you want fresh state each time):

    <.dialog :if={@show_dialog} id="dialog" show={true} on_cancel={JS.push("close")}>
      ...
    </.dialog>

## Alert Dialog

Use `alert={true}` to prevent closing via backdrop click (escape still works):

    <.dialog id="confirm-delete" title="Delete item" alert={true}>
      <:trigger :let={attr}>
        <.button variant="destructive" {attr}>Delete</.button>
      </:trigger>
      <p>Are you sure? This cannot be undone.</p>
    </.dialog>

## Dialog Title

Use the `title` attribute to render a built-in dialog heading:

    <.dialog id="profile-dialog" title="Edit profile">
      <:trigger :let={attr}>
        <.button {attr}>Edit profile</.button>
      </:trigger>
      <p>Update your profile details.</p>
    </.dialog>

The close button is shown by default. Disable it with `show_close={false}`:

    <.dialog id="checkout-dialog" title="Checkout" show_close={false}>
      <p>Review your order before continuing.</p>
    </.dialog>

## Scrollable Body with Footer

The default dialog keeps the title and footer fixed while the body scrolls automatically:

    <.dialog id="activity-dialog" title="Recent activity" size="lg">
      <div class="space-y-4">
        <p :for={_ <- 1..12}>Scrollable content</p>
      </div>
      <:footer :let={%{hide: hide}}>
        <div class="flex justify-end gap-2">
          <.button variant="outline" phx-click={hide}>Close</.button>
          <.button>Save changes</.button>
        </div>
      </:footer>
    </.dialog>

## Dialog Sizes

Control max-width with the `size` attribute:

    <.dialog id="small" size="sm">...</.dialog>   # sm:max-w-sm
    <.dialog id="medium" size="md">...</.dialog>  # md:max-w-md (default)
    <.dialog id="large" size="lg">...</.dialog>   # lg:max-w-lg
    <.dialog id="xlarge" size="xl">...</.dialog>  # xl:max-w-xl

For more granular control, set `size=""` and use the `class` attribute for full control over dimensions:

    <.dialog id="wide" size="" class="max-w-[80vw] max-h-[80vh]">...</.dialog>

## Custom Content Slot

Override the default content container for full customization. This bypasses the built-in
title, scrollable body, and footer layout:

    <.dialog id="custom">
      <:trigger :let={attr}>
        <.button {attr}>Open</.button>
      </:trigger>
      <:content :let={{attrs, %{hide: hide}}}>
        <div class="my-custom-dialog-class" {attrs}>
          <p>Fully customized container</p>
          <.button phx-click={hide}>Close</.button>
        </div>
      </:content>
    </.dialog>

## Programmatic Show/Hide

Use `show_dialog/1` and `hide_dialog/1` functions directly:

    <.button phx-click={PUI.Dialog.show_dialog("my-dialog")}>Open</.button>
    <.button phx-click={PUI.Dialog.hide_dialog("my-dialog")}>Close</.button>

## Attributes

| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| `id` | `string` | required | Unique identifier for the dialog |
| `show` | `boolean` | `false` | Control visibility from server |
| `alert` | `boolean` | `false` | Prevent backdrop click dismiss |
| `size` | `string` | `"md"` | Max width: `"sm"`, `"md"`, `"lg"`, `"xl"`. Set to `""` for custom sizing via `class` |
| `title` | `string` | `nil` | Optional built-in title for the default dialog header |
| `show_close` | `boolean` | `true` | Show the built-in close button on default dialogs |
| `on_cancel` | `JS` | `%JS{}` | JS command to run on cancel |
| `class` | `string` | `""` | Additional CSS classes applied to the content container. Use with `size=""` for full custom sizing (e.g. `max-w-[80vw] max-h-[80vh]`) |
| `variant` | `string` | `"default"` | Visual variant: `"default"` or `"unstyled"` |

## Slots

| Slot | Description |
|------|-------------|
| `inner_block` | Main dialog body content (scrolls when needed in the default layout) |
| `footer` | Optional fixed footer for actions in the default layout |
| `trigger` | Button/element to open dialog (receives `phx-click` attr) |
| `content` | Override content container (receives attrs and hide/show) |

# `backdrop`

## Attributes

* `class` (`:string`) - Defaults to `""`.
* `is_unstyled` (`:boolean`) - Defaults to `false`.
* Global attributes are accepted.
## Slots

* `inner_block`

# `content`

## Attributes

* `id` (`:string`) (required)
* `class` (`:string`) - Defaults to `""`.
* `is_unstyled` (`:boolean`) - Defaults to `false`.
* `title` (`:string`) - Defaults to `nil`.
* `show_close` (`:boolean`) - Defaults to `true`.
* `hide` (`Phoenix.LiveView.JS`) - Defaults to `%Phoenix.LiveView.JS{ops: []}`.
* Global attributes are accepted.
## Slots

* `inner_block`
* `footer`

# `dialog`

## Attributes

* `id` (`:string`) (required)
* `on_cancel` (`Phoenix.LiveView.JS`) - Defaults to `%Phoenix.LiveView.JS{ops: []}`.
* `alert` (`:boolean`) - Defaults to `false`.
* `show` (`:boolean`) - Control dialog visibility from server. Defaults to `false`.
* `size` (`:string`) - Defaults to `"md"`.
* `title` (`:string`) - Defaults to `nil`.
* `show_close` (`:boolean`) - Defaults to `true`.
* `variant` (`:string`) - Defaults to `"default"`. Must be one of `"default"`, or `"unstyled"`.
* `class` (`:string`) - Defaults to `""`.
* Global attributes are accepted. Supports all globals plus: `["aria-label", "aria-labelledby", "aria-describedby"]`.
## Slots

* `inner_block`
* `footer` - Optional fixed footer for the default dialog layout.
* `trigger`
* `content` - To override the content container.

# `hide_dialog`

# `show_dialog`

---

*Consult [api-reference.md](api-reference.md) for complete listing*
