Dialog
A modal overlay that dims the window behind it and holds the reader on one focused task until they close it.
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
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.
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 { 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.
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.
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.
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.
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.
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
titleand described bydescription. 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 whendismiss-on-backdropis 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-labelprovides 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
Dialogmounted 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.