# Dialog

A modal overlay that dims the window behind it and holds the reader on one focused task until they close it.

```slint
import { Button } from "@glint/components/button.slint";
import { Dialog } from "@glint/components/dialog.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 560px;
    height: 320px;
    background: Tokens.color-background;

    VerticalLayout {
        alignment: center;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            Button {
                text: "Open settings";
                haspopup: true;
                clicked => { settings.open = true; }
            }
        }
    }

    settings := Dialog {
        width: parent.width;
        height: parent.height;
        title: "Settings";
        description: "Choose how this device behaves.";

        Text {
            text: "Everything between the Dialog's braces is its body.";
            color: Tokens.color-foreground;
            wrap: word-wrap;
        }
    }
}
```

## Usage

```slint
import { Dialog } from "@glint/components/dialog.slint";

export component AppWindow inherits Window {
    in-out property <bool> editing: false;
    callback reload-profile();

    VerticalLayout {
        // your screen
    }

    // Last child of the window, sized to it: Slint has no portals, so what
    // draws on top is what comes last.
    Dialog {
        width: parent.width;
        height: parent.height;
        open <=> root.editing;
        title: "Edit profile";
        description: "Change your details and save.";
        dismissed => { root.reload-profile(); }
    }
}
```

A `Dialog` is the whole overlay: the dimmed backdrop and the card on top of it. It covers the window it is mounted in, so it takes the window’s full size and goes **last** among the window’s children — a `Dialog` declared before your screen’s layout would be painted underneath it.

Opening it is setting `open` to true; it sets `open` back to false itself when the reader closes it, and calls `dismissed` on the way out. The button that opens it should set `haspopup`, which is how a trigger says a surface is about to appear over the page.

If you already have your own overlay root and want only the card, mount `DialogPanel` instead — it is the same surface without the backdrop and without the full-window mount.

## Examples

### Body content

Whatever you put inside the `Dialog` becomes the body of the card, between the title block and the footer. The panel is 420px wide and as tall as its content.

```slint
import { Button } from "@glint/components/button.slint";
import { Dialog } from "@glint/components/dialog.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 560px;
    height: 360px;
    background: Tokens.color-background;

    VerticalLayout {
        alignment: center;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            Button {
                text: "Invite a teammate";
                haspopup: true;
                clicked => { invite.open = true; }
            }
        }
    }

    invite := Dialog {
        width: parent.width;
        height: parent.height;
        title: "Invite a teammate";
        description: "They get access to this workspace as soon as they accept.";

        VerticalLayout {
            spacing: 8px;

            Text {
                text: "ana@example.com";
                color: Tokens.color-foreground;
                font-size: Tokens.typography-body-size;
            }

            Text {
                text: "An invitation is valid for seven days.";
                color: Tokens.color-muted-foreground;
                font-size: Tokens.typography-body-sm-size;
                wrap: word-wrap;
            }
        }
    }
}
```

### Holding the reader until they choose

By default a click on the backdrop closes the modal. Set `dismiss-on-backdrop: false` when the dialog is asking for a decision that a stray click should not answer — the close button and `Escape` still work, so the reader is never trapped.

```slint
import { Button } from "@glint/components/button.slint";
import { Dialog } from "@glint/components/dialog.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 560px;
    height: 320px;
    background: Tokens.color-background;

    VerticalLayout {
        alignment: center;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            Button {
                text: "Review the terms";
                haspopup: true;
                clicked => { terms.open = true; }
            }
        }
    }

    terms := Dialog {
        width: parent.width;
        height: parent.height;
        title: "Before you continue";
        description: "Clicking outside will not close this one.";
        dismiss-on-backdrop: false;

        Text {
            text: "Use the close button or press Escape.";
            color: Tokens.color-muted-foreground;
            wrap: word-wrap;
        }
    }
}
```

### Naming the close button

`close-label` sets the accessible name of the corner close button and the text of the built-in button in the panel’s footer. It ships as `@tr("Close")` — translated with the rest of Glint — and you override it when the action deserves a name of its own.

```slint
import { Button } from "@glint/components/button.slint";
import { Dialog } from "@glint/components/dialog.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 560px;
    height: 320px;
    background: Tokens.color-background;

    VerticalLayout {
        alignment: center;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            Button {
                text: "What's new";
                haspopup: true;
                clicked => { release.open = true; }
            }
        }
    }

    release := Dialog {
        width: parent.width;
        height: parent.height;
        title: "What's new in 2.4";
        description: "Faster sync, and a keyboard shortcut for everything.";
        close-label: "Got it";
    }
}
```

### Custom actions

`actions` lets you supply custom buttons for the panel footer as an array of `DialogAction` structs. `action(int)` fires with the index of the clicked action. Set `show-footer-close: false` to hide the default Close button when your custom actions are the only exits.

```slint
import { Button, ButtonVariant } from "@glint/components/button.slint";
import { Dialog } from "@glint/components/dialog.slint";
import { DialogAction } from "@glint/components/dialog-panel.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 560px;
    height: 320px;
    background: Tokens.color-background;

    in-out property <string> status: "Unsaved changes";

    VerticalLayout {
        alignment: center;
        spacing: 12px;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            Button {
                text: "Save project";
                haspopup: true;
                clicked => { save-dialog.open = true; }
            }
        }

        Text {
            text: root.status;
            color: Tokens.color-muted-foreground;
            horizontal-alignment: center;
        }
    }

    save-dialog := Dialog {
        width: parent.width;
        height: parent.height;
        title: "Save changes?";
        description: "Your edits will be published to the workspace.";
        show-footer-close: false;
        actions: [
            { label: "Discard", variant: ButtonVariant.outline },
            { label: "Save", variant: ButtonVariant.default }
        ];
        action(index) => {
            if (index == 0) {
                root.status = "Changes discarded";
            } else {
                root.status = "Changes saved";
            }
            self.open = false;
        }
    }
}
```

### Restoring keyboard focus

`restore-focus` fires on every close — backdrop click, Escape, close button, or setting `open = false` programmatically. Slint has no automatic focus restoration, so wire `restore-focus` to return keyboard focus to the trigger that opened the dialog.

```slint
import { Button } from "@glint/components/button.slint";
import { Dialog } from "@glint/components/dialog.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 560px;
    height: 320px;
    background: Tokens.color-background;

    VerticalLayout {
        alignment: center;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            trigger := Button {
                text: "Manage account";
                haspopup: true;
                clicked => { account-dialog.open = true; }
            }
        }
    }

    account-dialog := Dialog {
        width: parent.width;
        height: parent.height;
        title: "Account settings";
        description: "Focus returns to the trigger button when dismissed.";
        restore-focus => { trigger.focus(); }
    }
}
```

### Driving it from your own state

`open` is two-way, so it binds to a property of yours: your screen decides when the dialog appears. `dismissed` reports closes the dialog initiates — the backdrop, the close button and `Escape` alike. Setting the bound property to `false` closes it without firing `dismissed`; `restore-focus` still fires for both paths.

```slint
import { Button } from "@glint/components/button.slint";
import { Dialog } from "@glint/components/dialog.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 560px;
    height: 320px;
    background: Tokens.color-background;

    in-out property <bool> editing: false;
    in-out property <int> closes: 0;

    VerticalLayout {
        alignment: center;
        spacing: 12px;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            Button {
                text: "Edit profile";
                haspopup: true;
                clicked => { root.editing = true; }
            }
        }

        Text {
            text: root.editing ? "The dialog is open"
                : root.closes == 0 ? "The dialog has not been opened yet"
                : "Closed " + root.closes + "×";
            color: Tokens.color-muted-foreground;
            horizontal-alignment: center;
        }
    }

    Dialog {
        width: parent.width;
        height: parent.height;
        open <=> root.editing;
        title: "Edit profile";
        description: "Your screen owns the state; the dialog reports the close.";
        dismissed => { root.closes += 1; }
    }
}
```

## API Reference

### Properties

| Property              | Type                | Default        | Description                                                                                                                                                                   |
| --------------------- | ------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `open`                | `in-out bool`       | `false`        | Two-way; consumer sets true to show, Glint sets false on close.                                                                                                               |
| `title`               | `in string`         | no default     | Heading at the top of the modal.                                                                                                                                              |
| `description`         | `in string`         | no default     | Body text under the title.                                                                                                                                                    |
| `dismiss-on-backdrop` | `in bool`           | `true`         | When true, clicking outside the panel closes the modal.                                                                                                                       |
| `close-label`         | `in string`         | `@tr("Close")` | Accessible label for the X-close button, and the text of the built-in footer Close button.                                                                                    |
| `show-close-button`   | `in bool`           | `true`         | Whether the top-right X is rendered.                                                                                                                                          |
| `show-footer-close`   | `in bool`           | `true`         | Whether the footer carries the built-in Close button. Turn both off for a dialog whose own `actions` are the only exits.                                                      |
| `actions`             | `in [DialogAction]` | `[]`           | The footer's own actions — Cancel + Save and the like — left to right, ahead of the built-in Close button.                                                                    |
| `panel-width`         | `in length`         | `420px`        | Width of the panel.                                                                                                                                                           |
| `panel-max-height`    | `in length`         | `0px`          | Ceiling on the panel's height; `0` (the default) lets it grow with its content. Past the ceiling the body scrolls under a pinned footer.                                      |
| `content-takes-focus` | `in bool`           | `false`        | Whether the body slotted into `@children` answers the keyboard itself — a form does, a paragraph of text does not. See `DialogPanel`, which the modal hands this straight to. |

### Callbacks

| Callback          | Description                                                                                                                                                                                                                                                                                                                |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dismissed()`     | Fired when the modal closes itself — backdrop, Escape, the X or the footer Close button.                                                                                                                                                                                                                                   |
| `action(int)`     | Fired when one of `actions` is pressed, with its index. The dialog does not close itself: whether Save closes and Cancel does not is the consumer's decision.                                                                                                                                                              |
| `restore-focus()` | Fired on EVERY close, including one the consumer drives by setting `open = false`, so keyboard focus always gets a new home: `restore-focus => { my-button.focus(); }`. Slint exposes no "previously focused element", so only the trigger's owner can name it. Same contract `Drawer`, `Sheet` and `AlertDialog` publish. |

### DialogPanel

The card without the backdrop and without the full-window mount, for a screen that already has an overlay root of its own. Its host is what decides where it sits and when it is shown.

### Properties

| Property              | Type                | Default        | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| --------------------- | ------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `title`               | `in string`         | no default     | Heading at the top of the panel.                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `description`         | `in string`         | no default     | Body text under the title.                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `close-label`         | `in string`         | `@tr("Close")` | Accessible label for the X-close button, and the text of the built-in footer Close button.                                                                                                                                                                                                                                                                                                                                                                     |
| `show-close-button`   | `in bool`           | `true`         | Whether the top-right X is rendered. Turn it off — together with `show-footer-close` — for a dialog whose own actions are the only exits.                                                                                                                                                                                                                                                                                                                      |
| `show-footer-close`   | `in bool`           | `true`         | Whether the footer carries the built-in Close button.                                                                                                                                                                                                                                                                                                                                                                                                          |
| `actions`             | `in [DialogAction]` | `[]`           | The footer's own actions, left to right, ahead of the built-in Close.                                                                                                                                                                                                                                                                                                                                                                                          |
| `panel-width`         | `in length`         | `420px`        | Width of the card.                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `panel-max-height`    | `in length`         | `0px`          | Ceiling on the card's height; `0` (the default) lets it grow with its content. Past the ceiling the body scrolls and the footer stays put.                                                                                                                                                                                                                                                                                                                     |
| `trap-focus`          | `in bool`           | `true`         | Whether Tab cycles inside the panel instead of leaving it. Escape closes either way — it is the modal's exit, not part of the trap.                                                                                                                                                                                                                                                                                                                            |
| `content-takes-focus` | `in bool`           | `false`        | Whether the body slotted into `@children` answers the keyboard itself — a form does, a paragraph of text does not. The panel cannot answer this for itself: the body is the consumer's, and Slint's tab walk is pre-order, so the body's scrolling surface would take a stop \*in front of\* the first field. Left false the surface takes the stop and the arrows scroll it, which is right for a long inert body and costs one extra Tab in front of a form. |

### Callbacks

| Callback      | Description                                             |
| ------------- | ------------------------------------------------------- |
| `closed()`    | Fired when the X or the footer Close button is pressed. |
| `action(int)` | Fired when one of `actions` is pressed, with its index. |

### Functions

| Function          | Description                                                                                                                                                                                                                                                            |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `take-focus()`    | Parks the keyboard just ahead of the body, so Escape has somewhere to land and the first Tab steps onto the first body control.                                                                                                                                        |
| `release-focus()` | Gives the keyboard up again. Taking the focus before dropping it is what makes this work from anywhere: `clear-focus()` only speaks for the scope that holds the focus, and on close that is usually one of the consumer's body controls, which this file cannot name. |

## Accessibility

- **Role and name.** Slint has no dialog role, so the open panel claims the closest landmark it has — a region — named by `title` and described by `description`. That is the surface assistive technology lands on, and giving both properties a value is what makes it findable by name.
- **Focus.** Opening the dialog moves focus into the panel; while it is open, Tab is trapped there, so the keyboard cannot wander into the screen behind the backdrop.
- **Keyboard.** `Escape` closes the dialog and fires `dismissed`, including when `dismiss-on-backdrop` is false — that property governs the backdrop click and nothing else, so a modal a pointer cannot dismiss is still one a keyboard can leave.
- **Closing controls.** `close-label` provides the accessible name for both the top-right close button (X) and the built-in footer button. The X is a fully accessible button in the tab order and accessibility tree, so screen readers and keyboard users have clear exits alongside `Escape`.
- **A closed dialog is not there.** The panel leaves the accessibility tree once it has faded out — a screen reader may only find what a sighted reader can see — and the backdrop stops intercepting clicks, so a closed `Dialog` mounted over your screen does not block it.
- **The trigger.** The control that opens a dialog should set `haspopup`, so that what it does is announced before it is pressed.
