/Documentation

Dialog

PublishedUpdated

A dialog asks for a decision, or holds a short task, before anything else continues: discard a draft, confirm a deletion, pick a ringtone. It is modal: the page behind it is out of reach until it closes. For a message that needs no answer, use a snackbar. See the M3 dialogs guidelines.

Usage

The buttons are its actions, and each closes it. layer: 'top' shows it in the browser's top layer, as a native <dialog> above everything else; the web component always is.

Vanilla
import { createDialog } from 'mtrl';

const dialog = createDialog({
  title: 'Discard draft?',
  content: 'Your draft will be deleted.',
  layer: 'top',
  buttons: [
    { text: 'Cancel', variant: 'text', closeDialog: true },
    { text: 'Discard', variant: 'text', closeDialog: true },
  ],
});
document.body.append(dialog.element);

function ask() {
  dialog.open();
}
Web Components
<m-dialog headline="Discard draft?">
  <p>Your draft will be deleted.</p>
  <m-button slot="actions" variant="text">Cancel</m-button>
  <m-button slot="actions" variant="text">Discard</m-button>
</m-dialog>

<script type="module">
  const dialog = document.querySelector('m-dialog');
  dialog.querySelectorAll('m-button[slot="actions"]').forEach((child) => child.addEventListener('click', () => dialog.close()));

  function ask() {
    dialog.show();
  }
</script>
React
import { useState } from 'react';
import { Dialog, Button } from 'mtrl/react';

export function Example() {
  const [open, setOpen] = useState(false);
  const ask = () => setOpen(true);

  return (
    <Dialog open={open} onClose={() => setOpen(false)} headline="Discard draft?">
      <p>Your draft will be deleted.</p>
      <Button slot="actions" variant="text" onClick={() => setOpen(false)}>Cancel</Button>
      <Button slot="actions" variant="text" onClick={() => setOpen(false)}>Discard</Button>
    </Dialog>
  );
}
Vue
<script setup lang="ts">
import { ref } from 'vue';
import { MDialog, MButton } from 'mtrl/vue';

const open = ref(false);

function ask() {
  open.value = true;
}
</script>

<template>
  <MDialog :open="open" @close="open = false" headline="Discard draft?">
    <p>Your draft will be deleted.</p>
    <template #actions>
      <MButton variant="text" @click="open = false">Cancel</MButton>
      <MButton variant="text" @click="open = false">Discard</MButton>
    </template>
  </MDialog>
</template>
Svelte
<script lang="ts">
  import { Dialog, Button } from 'mtrl/svelte';

  let open = $state(false);

  function ask() {
    open = true;
  }
</script>

<Dialog open={open} onclose={() => (open = false)} headline="Discard draft?">
  <p>Your draft will be deleted.</p>
  {#snippet actions()}
    <Button variant="text" onclick={() => (open = false)}>Cancel</Button>
    <Button variant="text" onclick={() => (open = false)}>Discard</Button>
  {/snippet}
</Dialog>
SolidJS
import { createSignal } from 'solid-js';
import { Dialog, Button } from 'mtrl/solid';

export function Example() {
  const [open, setOpen] = createSignal(false);
  const ask = () => setOpen(true);

  return (
    <Dialog open={open()} onClose={() => setOpen(false)} headline="Discard draft?">
      <p>Your draft will be deleted.</p>
      <Button slot="actions" variant="text" onClick={() => setOpen(false)}>Cancel</Button>
      <Button slot="actions" variant="text" onClick={() => setOpen(false)}>Discard</Button>
    </Dialog>
  );
}

Examples

Full screen

A full-screen dialog holds a task in a compact window. It has a close button in its header, and is a dialog rather than an alertdialog.

Vanilla
import { createDialog } from 'mtrl';

const dialog = createDialog({
  title: 'New event',
  content: 'Add a title, a time and guests.',
  size: 'fullscreen',
  layer: 'top',
  buttons: [{ text: 'Save', variant: 'text', closeDialog: true }],
});
document.body.append(dialog.element);
Web Components
<m-dialog size="fullscreen" headline="New event">
  <p>Add a title, a time and guests.</p>
  <m-button slot="actions" variant="text">Save</m-button>
</m-dialog>

<script type="module">
  const dialog = document.querySelector('m-dialog');
  dialog.querySelectorAll('m-button[slot="actions"]').forEach((child) => child.addEventListener('click', () => dialog.close()));
</script>
React
import { Dialog, Button } from 'mtrl/react';

export function Example() {
  return (
    <Dialog size="fullscreen" headline="New event">
      <p>Add a title, a time and guests.</p>
      <Button slot="actions" variant="text">Save</Button>
    </Dialog>
  );
}
Vue
<script setup lang="ts">
import { MDialog, MButton } from 'mtrl/vue';
</script>

<template>
  <MDialog size="fullscreen" headline="New event">
    <p>Add a title, a time and guests.</p>
    <template #actions>
      <MButton variant="text">Save</MButton>
    </template>
  </MDialog>
</template>
Svelte
<script lang="ts">
  import { Dialog, Button } from 'mtrl/svelte';
</script>

<Dialog size="fullscreen" headline="New event">
  <p>Add a title, a time and guests.</p>
  {#snippet actions()}
    <Button variant="text">Save</Button>
  {/snippet}
</Dialog>
SolidJS
import { Dialog, Button } from 'mtrl/solid';

export function Example() {
  return (
    <Dialog size="fullscreen" headline="New event">
      <p>Add a title, a time and guests.</p>
      <Button slot="actions" variant="text">Save</Button>
    </Dialog>
  );
}

Only its actions close it

For work that would be lost, the scrim and Escape can be kept from closing it; its actions then have to include a way out.

Vanilla
import { createDialog } from 'mtrl';

const dialog = createDialog({
  title: 'Leave the survey?',
  content: 'Your answers so far will be lost.',
  closeOnOverlayClick: false,
  closeOnEscape: false,
  layer: 'top',
  buttons: [
    { text: 'Stay', variant: 'text', closeDialog: true },
    { text: 'Leave', variant: 'text', closeDialog: true },
  ],
});
document.body.append(dialog.element);
Web Components
<m-dialog headline="Leave the survey?" no-close-on-scrim-click no-close-on-escape>
  <p>Your answers so far will be lost.</p>
  <m-button slot="actions" variant="text">Stay</m-button>
  <m-button slot="actions" variant="text">Leave</m-button>
</m-dialog>

<script type="module">
  const dialog = document.querySelector('m-dialog');
  dialog.querySelectorAll('m-button[slot="actions"]').forEach((child) => child.addEventListener('click', () => dialog.close()));
</script>
React
import { Dialog, Button } from 'mtrl/react';

export function Example() {
  return (
    <Dialog headline="Leave the survey?" noCloseOnScrimClick noCloseOnEscape>
      <p>Your answers so far will be lost.</p>
      <Button slot="actions" variant="text">Stay</Button>
      <Button slot="actions" variant="text">Leave</Button>
    </Dialog>
  );
}
Vue
<script setup lang="ts">
import { MDialog, MButton } from 'mtrl/vue';
</script>

<template>
  <MDialog headline="Leave the survey?" no-close-on-scrim-click no-close-on-escape>
    <p>Your answers so far will be lost.</p>
    <template #actions>
      <MButton variant="text">Stay</MButton>
      <MButton variant="text">Leave</MButton>
    </template>
  </MDialog>
</template>
Svelte
<script lang="ts">
  import { Dialog, Button } from 'mtrl/svelte';
</script>

<Dialog headline="Leave the survey?" noCloseOnScrimClick noCloseOnEscape>
  <p>Your answers so far will be lost.</p>
  {#snippet actions()}
    <Button variant="text">Stay</Button>
    <Button variant="text">Leave</Button>
  {/snippet}
</Dialog>
SolidJS
import { Dialog, Button } from 'mtrl/solid';

export function Example() {
  return (
    <Dialog headline="Leave the survey?" noCloseOnScrimClick noCloseOnEscape>
      <p>Your answers so far will be lost.</p>
      <Button slot="actions" variant="text">Stay</Button>
      <Button slot="actions" variant="text">Leave</Button>
    </Dialog>
  );
}

Subtitle and dividers

divider draws a line above and below the content, for a body that scrolls.

Vanilla
import { createDialog } from 'mtrl';

const dialog = createDialog({
  title: 'Terms of service',
  subtitle: 'Updated September 2026',
  content: 'These terms apply to your use of the service.',
  divider: true,
  layer: 'top',
  buttons: [{ text: 'Accept', variant: 'text', closeDialog: true }],
});
document.body.append(dialog.element);
Web Components
<m-dialog subtitle="Updated September 2026" divider headline="Terms of service">
  <p>These terms apply to your use of the service.</p>
  <m-button slot="actions" variant="text">Accept</m-button>
</m-dialog>

<script type="module">
  const dialog = document.querySelector('m-dialog');
  dialog.querySelectorAll('m-button[slot="actions"]').forEach((child) => child.addEventListener('click', () => dialog.close()));
</script>
React
import { Dialog, Button } from 'mtrl/react';

export function Example() {
  return (
    <Dialog subtitle="Updated September 2026" divider headline="Terms of service">
      <p>These terms apply to your use of the service.</p>
      <Button slot="actions" variant="text">Accept</Button>
    </Dialog>
  );
}
Vue
<script setup lang="ts">
import { MDialog, MButton } from 'mtrl/vue';
</script>

<template>
  <MDialog subtitle="Updated September 2026" divider headline="Terms of service">
    <p>These terms apply to your use of the service.</p>
    <template #actions>
      <MButton variant="text">Accept</MButton>
    </template>
  </MDialog>
</template>
Svelte
<script lang="ts">
  import { Dialog, Button } from 'mtrl/svelte';
</script>

<Dialog subtitle="Updated September 2026" divider headline="Terms of service">
  <p>These terms apply to your use of the service.</p>
  {#snippet actions()}
    <Button variant="text">Accept</Button>
  {/snippet}
</Dialog>
SolidJS
import { Dialog, Button } from 'mtrl/solid';

export function Example() {
  return (
    <Dialog subtitle="Updated September 2026" divider headline="Terms of service">
      <p>These terms apply to your use of the service.</p>
      <Button slot="actions" variant="text">Accept</Button>
    </Dialog>
  );
}

A button's onClick(event, dialog) runs its action, and returning false keeps the dialog open; the web component's actions are buttons of your own. confirm({ message }) asks a question in the dialog and resolves to the answer. Recipes such as confirming a deletion or a form that validates before it closes are planned for Examples.

API

Options

Option Type Default Description
title string undefined The headline, which names the dialog
subtitle string undefined A line below the headline
content string undefined The body, as HTML
buttons DialogButton[] [] The actions, in order
size 'small' | 'medium' | 'large' | 'fullwidth' | 'fullscreen' 'medium' The width; fullscreen fills the viewport
animation 'scale' | 'slide-up' | 'slide-down' | 'fade' 'scale' How it enters and leaves
footerAlignment 'right' | 'left' | 'center' | 'space-between' 'right' Where the actions sit
divider boolean false Lines above and below the content
closeButton boolean true at fullscreen, else false A close button in the header
open boolean false Whether it starts open
closeOnOverlayClick boolean true Whether a click on the scrim closes it
closeOnEscape boolean true Whether Escape closes it
layer 'top' undefined Shows it in the top layer, as a native <dialog> with showModal(); without it, it is in an overlay of its own in container
container HTMLElement document.body Where it is mounted
modal boolean true false leaves out aria-modal, and the page behind it keeps scrolling; outside the top layer it stays reachable too
autofocus boolean true Focuses it when it opens
trapFocus boolean true Keeps Tab inside it
role 'alertdialog' | 'dialog' by size The role, dialog at fullscreen and alertdialog otherwise
ariaLabel string undefined The name without a title
animationDuration number 500 to open, 150 to close When afteropen and afterclose follow, in ms
zIndex number undefined The overlay's z-index, outside the top layer
on { [event]: handler } undefined Event handlers registered at creation
class string undefined Additional CSS classes

A button

Option Type Default Description
text string required The label
variant string 'text' The button's variant
onClick (event, dialog) => void | boolean undefined Its action; false keeps the dialog open
closeDialog boolean true Whether it closes the dialog
autofocus boolean false Focuses it when the dialog opens
attributes Record<string, unknown> undefined More of the button's options
size string undefined The button's size
color string undefined Deprecated, never applied: the button has no colour option

Methods

Method Parameters Returns Description
open() / close() / toggle(open?) open?: boolean DialogComponent Opens or closes it
isOpen() none boolean Whether it is open
setTitle(title) / getTitle() title: string DialogComponent / string The headline
setSubtitle(subtitle) / getSubtitle() subtitle: string DialogComponent / string The subtitle
setContent(content) / getContent() content: string DialogComponent / string The body, as HTML
addButton(button) / removeButton(indexOrText) button: DialogButton, indexOrText: number | string DialogComponent Adds or removes an action
getButtons() none DialogButton[] The actions
setSize(size) / setFooterAlignment(alignment) size: DialogSize, alignment: DialogFooterAlignment DialogComponent The width, where the actions sit
toggleDivider(show) / hasDivider() show: boolean DialogComponent / boolean The lines around the content
getHeaderElement() / getContentElement() / getFooterElement() none HTMLElement | null Its regions
confirm(options) { message, title?, confirmText?, cancelText?, confirmVariant?, cancelVariant?, size? } Promise<boolean> Replaces the content and actions with a question, opens it, and resolves to the button pressed: true for the confirming one, which comes last, false for the other. Closed any other way (Escape, the scrim, close()), it resolves false. The message is text
on(event, handler) / off(event, handler) event: string, handler: Function DialogComponent Adds or removes a listener
destroy() none void Removes it
Property Type Description
element HTMLElement The dialog
overlay HTMLElement The scrim it sits in, outside the top layer

Events

Event Description Data
beforeopen / beforeclose It is about to open or close; preventDefault() stops it { dialog, preventDefault, defaultPrevented }
open / close It opened or closed { dialog }
afteropen / afterclose Its animation is over { dialog }

The web component dispatches open and close, without a detail, and cancel when Escape asks it to close, which preventDefault() refuses.

Accessibility

  • An alertdialog, or a dialog at fullscreen, with aria-modal. The title names it (aria-labelledby), or ariaLabel without one; the content describes it.
  • Opening it focuses its first focusable element, or the dialog; closing it gives focus back. Tab stays inside it, and the page behind it is inert.
  • Escape and a click on the scrim close it, unless turned off. The close button is named "Close dialog".
  • Its motion is reduced to a fade under prefers-reduced-motion.

Styling

.mtrl-dialog, .mtrl-dialog--visible { }
.mtrl-dialog--small, .mtrl-dialog--large, .mtrl-dialog--fullwidth, .mtrl-dialog--fullscreen { }
.mtrl-dialog--slide-up, .mtrl-dialog--slide-down, .mtrl-dialog--fade { }
.mtrl-dialog__overlay { }                 /* the scrim, outside the top layer */
.mtrl-dialog__header, .mtrl-dialog__header-title, .mtrl-dialog__header-subtitle { }
.mtrl-dialog__header-close, .mtrl-dialog__content, .mtrl-dialog__footer { }
.mtrl-dialog__footer--left, .mtrl-dialog__footer--center, .mtrl-dialog__footer--space-between { }
.mtrl-dialog__header-divider, .mtrl-dialog__footer-divider { }

Measurements

Attribute Value
Container surface-container-high, 28dp corners, elevation 3
Width 280dp to 560dp; small 360dp at most
Scrim scrim at 32%
Headline Headline Small, on-surface
Supporting text Body Medium, on-surface-variant
Padding 24dp; 16dp between the headline and the content
Actions 8dp apart
Full screen No corners; 56dp header, Title Large