Skip to main content

Dialog & ConfirmDialog

Modal overlays with focus trap, body scroll lock, and keyboard support. Dialog gives you full control over header, body, and footer slots. ConfirmDialog is an opinionated yes/no alert.

Quick start
import { Dialog, ConfirmDialog } from "mates-ui";
import { atom } from "mates";





// ── Template mode — include in html`` (visibility via openAtom) ──────────
const open = atom(false);

Dialog({
  openAtom: open,
  title: "Confirm delete",
  body: html`<p>This cannot be undone.</p>`,
  actions: [
    Button("Cancel", { variant: "ghost",  on: { click: () => open.set(false) } }),
    Button("Delete", { variant: "danger", on: { click: () => handleDelete() } }),
  ],
  onClose: () => open.set(false),
});

// ── Imperative mode — call from anywhere, no template needed ─────────────
Dialog({
  title: "File deleted",
  body: html`<p>Move to trash or delete permanently?</p>`,
  actions: [
    Button("Trash",  { variant: "ghost",  on: { click: () => {} } }),
    Button("Delete", { variant: "danger", on: { click: () => {} } }),
  ],
});

Standard dialog

import { html, atom, renderApp } from 'mates';
import { Button, Dialog, Text } from 'mates-ui';

const App = () => {
const open = atom(false);
return () => html`
<x-row gap="var(--md-space-3)">
${Button("Open dialog", { variant: "outlined", on: { click: () => open.set(true) } })}
</x-row>
${Dialog({
openAtom: open,
title: "Edit profile",
body: html`
${Text("Update your display name and bio.", { type: "body-md", color: "muted" })}
`,
actions: [
Button("Cancel", { variant: "ghost", on: { click: () => open.set(false) } }),
Button("Save", { variant: "filled", on: { click: () => open.set(false) } }),
],
onClose: () => open.set(false),
})}
`;
};

renderApp(App, document.getElementById('app'));

With close button

import { html, atom, renderApp } from 'mates';
import { Button, Dialog, Text } from 'mates-ui';

const App = () => {
const open = atom(false);
return () => html`
<x-row gap="var(--md-space-3)">
${Button("Open dialog", { variant: "outlined", on: { click: () => open.set(true) } })}
</x-row>
${Dialog({
openAtom: open,
title: "Notification settings",
closeButton: true,
body: html`
${Text("Choose which notifications you want to receive.", { type: "body-md", color: "muted" })}
`,
actions: [
Button("Save preferences", { variant: "filled", on: { click: () => open.set(false) } }),
],
onClose: () => open.set(false),
})}
`;
};

renderApp(App, document.getElementById('app'));

Alert variant — with icon

import { html, atom, renderApp } from 'mates';
import { Button, Dialog, Text } from 'mates-ui';
import { WarningIcon } from 'mates-icons';

const App = () => {
const open = atom(false);
return () => html`
<x-row gap="var(--md-space-3)">
${Button("Warning alert", { variant: "outlined", on: { click: () => open.set(true) } })}
</x-row>
${Dialog({
openAtom: open,
variant: "alert",
title: "Unsaved changes",
icon: WarningIcon({ color: "var(--md-color-warning)" }),
body: html`
${Text("You have unsaved changes. Leaving this page will discard them.", { type: "body-md", color: "muted" })}
`,
actions: [
Button("Keep editing", { variant: "ghost", on: { click: () => open.set(false) } }),
Button("Discard", { variant: "danger", on: { click: () => open.set(false) } }),
],
onClose: () => open.set(false),
})}
`;
};

renderApp(App, document.getElementById('app'));

Header and footer dividers

import { html, atom, renderApp } from 'mates';
import { Button, Dialog, Text } from 'mates-ui';

const App = () => {
const open = atom(false);
return () => html`
<x-row gap="var(--md-space-3)">
${Button("Open dialog", { variant: "outlined", on: { click: () => open.set(true) } })}
</x-row>
${Dialog({
openAtom: open,
title: "Terms of service",
headerDivider: true,
footerDivider: true,
body: html`
<x-col gap="var(--md-space-3)">
${Text("Section 1 — Acceptance of terms", { type: "label-lg" })}
${Text("By accessing or using our service, you agree to be bound by these terms.", { type: "body-md", color: "muted" })}
${Text("Section 2 — Use of service", { type: "label-lg" })}
${Text("You may use our service only for lawful purposes.", { type: "body-md", color: "muted" })}
</x-col>
`,
actions: [
Button("Decline", { variant: "ghost", on: { click: () => open.set(false) } }),
Button("Accept", { variant: "filled", on: { click: () => open.set(false) } }),
],
onClose: () => open.set(false),
})}
`;
};

renderApp(App, document.getElementById('app'));

Scrollable body

import { html, atom, renderApp } from 'mates';
import { Button, Dialog, Text } from 'mates-ui';

const NOTES = [
"New virtualised table with 100k row support.",
"VerticalSegments component for resizable pane layouts.",
"disappear directive — removes elements after a delay.",
"changeFlagAtom — forces re-renders on demand.",
"Button default height reduced to 36 px.",
"Secondary button variant removed.",
"Pill tabs background moved to each tab item.",
];

const App = () => {
const open = atom(false);
return () => html`
<x-row gap="var(--md-space-3)">
${Button("Open dialog", { variant: "outlined", on: { click: () => open.set(true) } })}
</x-row>
${Dialog({
openAtom: open,
title: "Release notes — v4.2.0",
closeButton: true,
headerDivider: true,
footerDivider: true,
maxHeight: "min(70vh, 32rem)",
body: html`
<x-col gap="var(--md-space-3)">
${NOTES.map(note => Text(note, { type: "body-md", color: "muted" }))}
</x-col>
`,
actions: [
Button("Close", { variant: "filled", on: { click: () => open.set(false) } }),
],
onClose: () => open.set(false),
})}

Disabled scrim dismiss

import { html, atom, renderApp } from 'mates';
import { Button, Dialog, Text } from 'mates-ui';
import { InfoIcon } from 'mates-icons';

const App = () => {
const open = atom(false);
return () => html`
<x-row gap="var(--md-space-3)">
${Button("Open dialog", { variant: "outlined", on: { click: () => open.set(true) } })}
</x-row>
${Dialog({
openAtom: open,
title: "Required action",
icon: InfoIcon(),
scrimDismiss: false,
closeButton: false,
body: html`
${Text("You must complete this action before continuing. Clicking outside will not close this dialog.", { type: "body-md", color: "muted" })}
`,
actions: [
Button("Acknowledge", { variant: "filled", on: { click: () => open.set(false) } }),
],
onClose: () => open.set(false),
})}
`;
};

renderApp(App, document.getElementById('app'));

Custom width

import { html, atom, renderApp } from 'mates';
import { Button, Dialog, Text } from 'mates-ui';

const App = () => {
const open = atom(false);
return () => html`
<x-row gap="var(--md-space-3)">
${Button("Open wide dialog", { variant: "outlined", on: { click: () => open.set(true) } })}
</x-row>
${Dialog({
openAtom: open,
title: "Import data",
width: "40rem",
closeButton: true,
headerDivider: true,
footerDivider: true,
body: html`
<x-col gap="var(--md-space-3)">
${Text("Paste your CSV data below or drag and drop a file.", { type: "body-md", color: "muted" })}
<textarea rows="6" placeholder="id,name,email"
style="width:100%;box-sizing:border-box;padding:var(--md-space-3);border:1px solid var(--md-color-border);border-radius:var(--md-radius-sm);resize:vertical;background:var(--md-color-surface);color:var(--md-color-text);">
</textarea>
</x-col>
`,
actions: [
Button("Cancel", { variant: "ghost", on: { click: () => open.set(false) } }),
Button("Import", { variant: "filled", on: { click: () => open.set(false) } }),
],
onClose: () => open.set(false),
})}
`;
};

renderApp(App, document.getElementById('app'));

Fullscreen variant

import { html, atom, renderApp } from 'mates';
import { Button, Dialog } from 'mates-ui';

const App = () => {
const open = atom(false);
return () => html`
<x-row gap="var(--md-space-3)">
${Button("Open fullscreen", { variant: "outlined", on: { click: () => open.set(true) } })}
</x-row>
${Dialog({
openAtom: open,
variant: "fullscreen",
title: "Compose message",
fullscreenLeading: Button("Cancel", { variant: "ghost", size: "sm", on: { click: () => open.set(false) } }),
fullscreenTrailing: Button("Send", { variant: "filled", size: "sm", on: { click: () => open.set(false) } }),
body: html`
<textarea placeholder="Write your message…"
style="flex:1;width:100%;box-sizing:border-box;padding:var(--md-space-4);border:1px solid var(--md-color-border);border-radius:var(--md-radius-sm);resize:none;min-height:12rem;background:var(--md-color-surface);color:var(--md-color-text);">
</textarea>
`,
onClose: () => open.set(false),
})}
`;
};

renderApp(App, document.getElementById('app'));

Alert, Confirm & Prompt — browser-native replacements

import { html, atom, nothing, renderApp } from 'mates';
import { Alert, Button, Confirm, Prompt, Text } from 'mates-ui';

const App = () => {
const alertResult = atom('');
const confirmResult = atom('');
const promptResult = atom('');

return () => html`
<x-col gap="var(--md-space-5)">
<x-col gap="var(--md-space-2)">
${Text("Alert", { type: "label-md" })}
<x-row gap="var(--md-space-3)">
${Button("Show alert", { variant: "outlined", on: { click: async () => {
await Alert("Your changes have been saved.", { type: "success", okLabel: "Got it" });
alertResult.set("dismissed");
}}})}
${alertResult() ? Text("→ " + alertResult(), { type: "body-sm", color: "muted" }) : nothing}
</x-row>
</x-col>
<x-col gap="var(--md-space-2)">
${Text("Confirm", { type: "label-md" })}
<x-row gap="var(--md-space-3)">
${Button("Delete item", { variant: "danger", on: { click: async () => {
const ok = await Confirm("Delete this item?", { title: "Confirm delete", type: "danger", dangerMessage: "This cannot be undone." });
confirmResult.set(ok ? "confirmed ✔" : "cancelled ✖");
}}})}
${confirmResult() ? Text("→ " + confirmResult(), { type: "body-sm", color: "muted" }) : nothing}
</x-row>
</x-col>
<x-col gap="var(--md-space-2)">
${Text("Prompt", { type: "label-md" })}
<x-row gap="var(--md-space-3)">
${Button("Rename file", { variant: "outlined", on: { click: async () => {
const name = await Prompt("Enter a new file name:", { title: "Rename", placeholder: "my-file.ts", defaultValue: "untitled.ts", okLabel: "Rename" });
promptResult.set(name !== null ? '"' + name + '"' : "cancelled");

ConfirmDialog — basic

import { html, atom, nothing, renderApp } from 'mates';
import { Button, ConfirmDialog, Text } from 'mates-ui';

const App = () => {
const open = atom(false);
const result = atom('');
return () => html`
<x-col gap="var(--md-space-4)">
<x-row gap="var(--md-space-3)">
${Button("Publish post", { variant: "outlined", on: { click: () => open.set(true) } })}
${result() ? Text("→ " + result(), { type: "body-sm", color: "muted" }) : nothing}
</x-row>
${ConfirmDialog({
openAtom: open,
title: "Publish post?",
message: "This will make your post visible to all users.",
confirmLabel: "Publish",
onConfirm: () => result.set("Published!"),
onCancel: () => result.set("Cancelled"),
})}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

ConfirmDialog — destructive action

import { html, atom, nothing, renderApp } from 'mates';
import { Button, ConfirmDialog, Text } from 'mates-ui';
import { DeleteForeverIcon } from 'mates-icons';

const App = () => {
const open = atom(false);
const result = atom('');
return () => html`
<x-col gap="var(--md-space-4)">
<x-row gap="var(--md-space-3)">
${Button("Delete account", { variant: "danger", icon: DeleteForeverIcon(), on: { click: () => open.set(true) } })}
${result() ? Text("→ " + result(), { type: "body-sm", color: "muted" }) : nothing}
</x-row>
${ConfirmDialog({
openAtom: open,
title: "Delete account?",
message: "This will permanently delete your account. This action cannot be undone.",
confirmLabel: "Delete account",
cancelLabel: "Keep account",
danger: true,
onConfirm: () => result.set("Account deleted"),
onCancel: () => result.set("Kept account"),
})}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

ConfirmDialog — custom labels

import { html, atom, renderApp } from 'mates';
import { Button, ConfirmDialog } from 'mates-ui';

const App = () => {
const open = atom(false);
return () => html`
<x-row gap="var(--md-space-3)">
${Button("Leave page?", { variant: "outlined", on: { click: () => open.set(true) } })}
</x-row>
${ConfirmDialog({
openAtom: open,
title: "Leave without saving?",
message: "You have unsaved changes. If you leave now your changes will be lost.",
confirmLabel: "Leave",
cancelLabel: "Stay",
onConfirm: () => open.set(false),
onCancel: () => open.set(false),
})}
`;
};

renderApp(App, document.getElementById('app'));

Dialog / ConfirmDialog

import { Dialog, ConfirmDialog } from "mates-ui";
Prop Type Default Description
actions TemplateResult[] Legacy shorthand — array of action buttons appended to a right-aligned footer. Ignored when `footer` is set.
attr AttrMap HTML attributes on the root element.
body TemplateResult Main content area.
classes string | string[] Extra root classes — joined and appended after built-in classes.
closeButton boolean false Show the built-in × close button in the header.
data DataAttrMap Root `data-*` map (`testId` → `data-testid`). `testId` prop / `attr` win over `data`.
footer TemplateResult Custom footer. Pass FooterBar([...]) for the standard action button layout.
footerDivider boolean false Show a 1 px divider between body and footer.
headerDivider boolean false Show a 1 px divider between header and body.
icon IconInput Leading icon shown in alert / standard header.
id string Root `id` shorthand (`attr.id` wins). Form native-control `id` props are unchanged.
on OnEventMap DOM event handlers on the root element.
onClose () => void Called when the dialog requests to close — Escape key, close button, or scrim click.
open boolean Plain boolean — use when you re-render each frame (`open: myAtom()`).
openAtom AtomType<boolean> Reactive atom driving visibility. Preferred over `open`. Close sets it to false automatically.
scrimDismiss boolean true Click on the backdrop closes the dialog.
style StyleMap Inline CSS on the root element.
title string | TemplateResult Heading rendered inside the default header.
variant "standard" | "alert" | "fullscreen" "standard" Visual variant. Alert centers the content and icon.
width string "25rem" Panel width. Pass `auto` to size by content.

ConfirmDialog props

Prop Type Default Description
cancelLabel string "Cancel" Label for the cancel button.
confirmLabel string "Confirm" Label for the confirm button.
danger boolean false Use danger styling on the confirm button.
message* string | TemplateResult Body message.
onCancel () => void Called when the user cancels.
onConfirm* () => void Called when the user clicks the confirm button.
openAtom* AtomType<boolean> Atom controlling visibility.
title* string Dialog heading.

CSS tokens

Token Role
--md-color-border Divider color.
--md-color-surface Dialog panel background.
--md-radius-md Panel border radius.
--md-shadow-dialog Panel drop shadow.

Caveats

  • Dialog supports two modes: template mode (embed in html`` with openAtom/open) and imperative mode (call from any JS context — no atom needed, mounts to <body> and auto-removes on close).
  • Focus is trapped inside the dialog while open. Tab / Shift+Tab cycle focusable elements. Escape closes the dialog.
  • Body scroll is locked while a dialog is open.
  • For fullscreen variant, use fullscreenLeading / fullscreenTrailing slots instead of actions.