Skip to content

Containment

Dialogs

A modal window for an important prompt or a task that needs focus: alert dialogs and full-screen dialogs.

Examples

Alert dialog

A modal prompt: the page is inert, Tab stays inside, Escape or the scrim ask to close, and focus returns to the button that opened it.

The live demo runs with JavaScript. Its code is below.

React HTML
import { useState } from 'react';
import { AlertDialog, Button } from '@kyralabs/m3exp-web/react';

export default function Alert() {
  const [open, setOpen] = useState(false);
  const close = () => {
    setOpen(false);
  };
  return (
    <>
      <Button
        variant="tonal"
        onPress={() => {
          setOpen(true);
        }}
      >
        Discard draft
      </Button>
      <AlertDialog
        isOpen={open}
        onOpenChange={setOpen}
        icon="delete"
        headline="Discard draft?"
        dismissButton={
          <Button variant="text" onPress={close}>
            Cancel
          </Button>
        }
        confirmButton={
          <Button variant="text" onPress={close}>
            Discard
          </Button>
        }
      >
        This permanently deletes the draft.
      </AlertDialog>
    </>
  );
}
<button type="button" class="m3e-button m3e-button--tonal" id="open-discard">Discard draft</button>
<m3e-dialog id="discard" headline="Discard draft?" icon="delete">
  This permanently deletes the draft.
  <button type="button" class="m3e-button m3e-button--text" slot="actions">Cancel</button>
  <button type="button" class="m3e-button m3e-button--text" slot="actions">Discard</button>
</m3e-dialog>

<script type="module">
  const dialog = document.getElementById('discard');
  document.getElementById('open-discard').addEventListener('click', () => dialog.show());
  dialog
    .querySelectorAll('[slot="actions"]')
    .forEach((b) => b.addEventListener('click', () => dialog.close()));
</script>

Run the HTML version (custom elements, no framework).

Full-screen dialog

Fills the window below 600px; from 600px it is laid out as a basic dialog.

The live demo runs with JavaScript. Its code is below.

React HTML
import { useState } from 'react';
import { Button, FullScreenDialog, TextField } from '@kyralabs/m3exp-web/react';

export default function FullScreen() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <Button
        variant="outlined"
        onPress={() => {
          setOpen(true);
        }}
      >
        New event
      </Button>
      <FullScreenDialog
        isOpen={open}
        onOpenChange={setOpen}
        headline="New event"
        action={
          <Button
            variant="text"
            onPress={() => {
              setOpen(false);
            }}
          >
            Save
          </Button>
        }
      >
        <TextField label="Title" />
      </FullScreenDialog>
    </>
  );
}
<button type="button" class="m3e-button m3e-button--outlined" id="open-event">New event</button>
<m3e-dialog id="event" variant="full-screen" headline="New event">
  <m3e-text-field label="Title"></m3e-text-field>
  <button type="button" class="m3e-button m3e-button--text" slot="actions">Save</button>
</m3e-dialog>

<script type="module">
  const dialog = document.getElementById('event');
  document.getElementById('open-event').addEventListener('click', () => dialog.show());
  dialog.querySelector('[slot="actions"]').addEventListener('click', () => dialog.close());
</script>

Run the HTML version (custom elements, no framework).

API

React

<AlertDialog>

Alert dialog (COMPONENT_SPEC §29; Compose AlertDialog): an optional icon, a headline, the supporting text and the actions (the dismiss button, then the confirm button: end-aligned, the confirm button on top when they stack) in a 280–560dp surface over a scrim. A native <dialog role="alertdialog"> (m3.material.io's web role) opened with showModal() by m3e-elements' DialogLayout (the code <m3e-dialog> runs): the page is inert, Tab stays inside, focus starts on the first control and returns to the opener; Escape and the scrim ask to close (onOpenChange(false)). Other props go to the <dialog>.

Prop Type Default Description
headline ReactNode The title (Compose title); it names the dialog.
icon string A Material Symbols name shown above the headline, which is then centered.
confirmButton ReactNode The confirming action (Compose confirmButton), usually a text <Button>.
dismissButton ReactNode The dismissing action (Compose dismissButton); rendered before the confirm button.
aria-label string The dialog's name when it has no headline (default "Dialog", Compose's pane title).
children ReactNode The supporting text (Compose text); it describes the dialog.
isOpen boolean Open (controlled).
defaultOpen boolean The initial open state (uncontrolled).
onOpenChange (isOpen: boolean) => void Called when the dialog asks to close (Escape, a press on the scrim, the full-screen close button, a browser close request: Compose's onDismissRequest) with false. Controlled dialogs close when isOpen becomes false; leave it true to keep the dialog open.

Also accepts HTML attributes not listed here.

<BasicAlertDialog>

Dialog with custom content (COMPONENT_SPEC §29; Compose BasicAlertDialog): the children, sized 280–560dp, over a scrim, in a plain modal <dialog> named by aria-label (default "Dialog"). Same behavior as <AlertDialog>.

Prop Type Default Description
aria-label string The dialog's name (default "Dialog", Compose's pane title).
children ReactNode The whole dialog: it draws its own surface (e.g. a <Surface>).
isOpen boolean Open (controlled).
defaultOpen boolean The initial open state (uncontrolled).
onOpenChange (isOpen: boolean) => void Called when the dialog asks to close (Escape, a press on the scrim, the full-screen close button, a browser close request: Compose's onDismissRequest) with false. Controlled dialogs close when isOpen becomes false; leave it true to keep the dialog open.

Also accepts HTML attributes not listed here.

<FullScreenDialog>

Full-screen dialog (COMPONENT_SPEC §29; m3.material.io only, Compose has none): a header with a close button, the headline and the confirming action, then the content. Below 600dp (the medium window size class) it fills the window and the header takes the on-scroll color while the content scrolls; from 600dp it is laid out as a basic dialog (m3.material.io: "For medium and expanded breakpoints, use a basic dialog"). A plain modal <dialog>; the close button, Escape and the scrim ask to close (onOpenChange(false)). Same behavior as <AlertDialog>.

Prop Type Default Description
headline ReactNode The title in the header; it names the dialog.
action ReactNode The confirming action at the end of the header (m3.material.io: a text button, e.g. Save).
closeLabel string The close button's name (default "Close").
aria-label string The dialog's name when it has no headline (default "Dialog").
children ReactNode The content (scrolls under the header).
isOpen boolean Open (controlled).
defaultOpen boolean The initial open state (uncontrolled).
onOpenChange (isOpen: boolean) => void Called when the dialog asks to close (Escape, a press on the scrim, the full-screen close button, a browser close request: Compose's onDismissRequest) with false. Controlled dialogs close when isOpen becomes false; leave it true to keep the dialog open.

Also accepts HTML attributes not listed here.

Custom elements

<m3e-dialog>

import '@kyralabs/m3e-elements/dialog.js';

<m3e-dialog headline="Title" open>Text<button slot="actions">OK</button></m3e-dialog> (COMPONENT_SPEC §29; Compose AlertDialog / BasicAlertDialog and m3.material.io's full-screen dialog). Renders the m3e-css .m3e-dialog contract inside itself (a native <dialog>; the host is display: contents) and runs DialogLayout (shared with m3e-react): showModal(), the open / close motion, Tab kept inside, Escape / the scrim / the close button asking to close, focus returned to the opener.

  • variant: alert (default; role="alertdialog", m3.material.io's web role, named by the headline and described by the content), custom (BasicAlertDialog: the children are the whole dialog, which draws its own surface; a plain <dialog>), full-screen (a header with a close button and the headline; full-screen below 600dp, a basic dialog above; a plain <dialog>).
  • headline, icon (alert: a Material Symbols name above the headline), label (the dialog's name when it has no headline; default "Dialog", Compose's pane title), close-label (full-screen: the close button's name, default "Close").
  • Children with slot="actions" (text buttons; the dismiss button first) go into the actions row; the other child nodes are the content (the supporting text).
  • open (reflected). Methods show(), close(). Event open-change (detail: { open }), cancelable when Escape, the scrim, the close button or a browser close request asks to close (preventDefault() keeps it open; Compose's onDismissRequest).
Attribute Property Type Notes
open open boolean Reflected.
variant variant string Reflected.
headline headline string
icon icon string
label label string
close-label closeLabel string
Event Detail Description
open-change OpenChangeDetail Opened or closed. Cancelable when the user closes it (scrim, Escape, close button).

CSS

dialog.css

@import '@kyralabs/m3e-css/dialog.css';

Dialogs (COMPONENT_SPEC §29): the alert dialog (Compose AlertDialog), custom content (Compose BasicAlertDialog) and the full-screen dialog (m3.material.io; Compose has none at the pin). A native <dialog> opened with showModal() (top layer, background inert); the m3e-elements dialog controller (<m3e-dialog>, m3e-react's dialogs) opens and closes it with the motion below, keeps Tab inside, locks the page scroll, asks to close on Escape / the scrim and returns focus.

<dialog class="m3e-dialog" role="alertdialog" aria-labelledby="h" aria-describedby="t">
  <div class="m3e-dialog__scrim" aria-hidden="true"></div>
  <div class="m3e-dialog__container">
    <span class="m3e-icon m3e-dialog__icon" aria-hidden="true">favorite</span>  (optional)
    <h2 class="m3e-dialog__headline" id="h">Title</h2>                         (optional)
    <div class="m3e-dialog__content" id="t">Supporting text</div>              (optional)
    <div class="m3e-dialog__actions">
      <button class="m3e-button m3e-button--text" type="button">Dismiss</button>
      <button class="m3e-button m3e-button--text" type="button">Confirm</button>
    </div>
  </div>
</dialog>
  • --custom (BasicAlertDialog): the container only sizes and moves its content, which draws its
own surface. The dialog is a plain <dialog> named by aria-label (default "Dialog").
  • --full-screen: a __header (a standard icon button __close, then the __headline) before the
__content, and the __actions (a text button, e.g. Save). Below the medium window size class
(600dp) it fills the window, the actions sit at the end of the 56dp header and the content
scrolls under it; from 600dp it is laid out as a basic dialog (m3.material.io: "For medium
and expanded breakpoints, use a basic dialog"), the close button before the headline.
  • Buttons come from button.css / icon-button.css (import them as well). The dismiss button goes
first: the row is end-aligned and wraps upwards, so the confirm button sits at the end of a
row and on top when the buttons stack (AlertDialogFlowRow).
  • JS hooks: [data-motion='enter' | 'exit'] runs the open / close animations (the controller
closes the <dialog> when the exit ones end); [data-scrolled] (full-screen) while the content
is scrolled.
67 custom properties it reads
Custom property Default
--m3e-comp-dialog-action-label-text-color var(--m3e-sys-color-primary)
--m3e-comp-dialog-action-label-text-font var(--m3e-sys-typescale-label-large-font)
--m3e-comp-dialog-action-label-text-line-height var(--m3e-sys-typescale-label-large-line-height)
--m3e-comp-dialog-action-label-text-size var(--m3e-sys-typescale-label-large-size)
--m3e-comp-dialog-action-label-text-tracking var(--m3e-sys-typescale-label-large-tracking)
--m3e-comp-dialog-action-label-text-weight var(--m3e-sys-typescale-label-large-weight)
--m3e-comp-dialog-actions-cross-axis-spacing 8px
--m3e-comp-dialog-actions-enter-delay 150ms
--m3e-comp-dialog-actions-enter-duration 150ms
--m3e-comp-dialog-actions-main-axis-spacing 8px
--m3e-comp-dialog-button-min-height 40px
--m3e-comp-dialog-container-collapsed-height 35%
--m3e-comp-dialog-container-color var(--m3e-sys-color-surface-container-high)
--m3e-comp-dialog-container-enter-fade-duration 50ms
--m3e-comp-dialog-container-exit-fade-delay 100ms
--m3e-comp-dialog-container-exit-fade-duration 50ms
--m3e-comp-dialog-container-max-width 560px
--m3e-comp-dialog-container-min-width 280px
--m3e-comp-dialog-container-padding 24px
--m3e-comp-dialog-container-shape var(--m3e-sys-shape-corner-extra-large)
--m3e-comp-dialog-content-enter-delay 50ms
--m3e-comp-dialog-content-enter-duration 200ms
--m3e-comp-dialog-content-exit-duration 100ms
--m3e-comp-dialog-enter-duration 500ms
--m3e-comp-dialog-enter-easing var(--m3e-sys-motion-easing-emphasized)
--m3e-comp-dialog-exit-duration 150ms
--m3e-comp-dialog-exit-easing cubic-bezier(0.3, 0, 0.8, 0.15)
--m3e-comp-dialog-headline-color var(--m3e-sys-color-on-surface)
--m3e-comp-dialog-headline-font var(--m3e-sys-typescale-headline-small-font)
--m3e-comp-dialog-headline-line-height var(--m3e-sys-typescale-headline-small-line-height)
--m3e-comp-dialog-headline-padding-bottom 16px
--m3e-comp-dialog-headline-size var(--m3e-sys-typescale-headline-small-size)
--m3e-comp-dialog-headline-tracking var(--m3e-sys-typescale-headline-small-tracking)
--m3e-comp-dialog-headline-weight var(--m3e-sys-typescale-headline-small-weight)
--m3e-comp-dialog-icon-color var(--m3e-sys-color-secondary)
--m3e-comp-dialog-icon-padding-bottom 16px
--m3e-comp-dialog-icon-size 24px
--m3e-comp-dialog-scrim-duration var(--m3e-sys-motion-spring-default-effects-duration)
--m3e-comp-dialog-scrim-easing var(--m3e-sys-motion-spring-default-effects-easing)
--m3e-comp-dialog-slide-distance 50px
--m3e-comp-dialog-supporting-text-color var(--m3e-sys-color-on-surface-variant)
--m3e-comp-dialog-supporting-text-font var(--m3e-sys-typescale-body-medium-font)
--m3e-comp-dialog-supporting-text-line-height var(--m3e-sys-typescale-body-medium-line-height)
--m3e-comp-dialog-supporting-text-padding-bottom 24px
--m3e-comp-dialog-supporting-text-size var(--m3e-sys-typescale-body-medium-size)
--m3e-comp-dialog-supporting-text-tracking var(--m3e-sys-typescale-body-medium-tracking)
--m3e-comp-dialog-supporting-text-weight var(--m3e-sys-typescale-body-medium-weight)
--m3e-comp-dialog-window-margin 24px
--m3e-comp-divider-thickness 1px
--m3e-comp-full-screen-dialog-container-color var(--m3e-sys-color-surface)
--m3e-comp-full-screen-dialog-container-shape var(--m3e-sys-shape-corner-none)
--m3e-comp-full-screen-dialog-header-container-color var(--m3e-sys-color-surface)
--m3e-comp-full-screen-dialog-header-container-height 56px
--m3e-comp-full-screen-dialog-header-headline-color var(--m3e-sys-color-on-surface)
--m3e-comp-full-screen-dialog-header-headline-font var(--m3e-sys-typescale-title-large-font)
--m3e-comp-full-screen-dialog-header-headline-line-height var(--m3e-sys-typescale-title-large-line-height)
--m3e-comp-full-screen-dialog-header-headline-size var(--m3e-sys-typescale-title-large-size)
--m3e-comp-full-screen-dialog-header-headline-tracking var(--m3e-sys-typescale-title-large-tracking)
--m3e-comp-full-screen-dialog-header-headline-weight var(--m3e-sys-typescale-title-large-weight)
--m3e-comp-full-screen-dialog-header-icon-color var(--m3e-sys-color-on-surface)
--m3e-comp-full-screen-dialog-header-icon-size 24px
--m3e-comp-full-screen-dialog-header-on-scroll-container-color var(--m3e-sys-color-surface-container)
--m3e-comp-full-screen-dialog-header-spacing 8px
--m3e-comp-full-screen-dialog-padding 24px
--m3e-comp-minimum-interactive-component-size 48px
--m3e-comp-scrim-container-color var(--m3e-sys-color-scrim)
--m3e-comp-scrim-container-opacity 0.32

Differences from Compose

the motion is Material Web's, not Compose's (owner decision M9 #9), on androidx's emphasized easing (D20 (6)); the window margin is Material Web's 24dp, not Android's dialog insets; the stacked buttons wrap by width as FlowRow does but the web measures the labels with the browser's text metrics; the full-screen dialog's header layout (8dp paddings) and colors are the owner's picks from m3.material.io (D19 (13)), its on-scroll elevation and m3.material.io's bottom action bar (deprecated in the token DB) are not built, and from 600dp it keeps its close button above the content (m3.material.io shows no anatomy for the swapped basic dialog).

Sources

Values come from androidx Compose Material3 1.5.0-beta01 at 19991f2c and from m3.material.io, as cited below from COMPONENT_SPEC §29 and the m3e-css sources.

AlertDialog.kt, AndroidAlertDialog.android.kt, AlertDialogSamples.kt, ModalBottomSheet.kt, Button.kt (ButtonDefaults.MinHeight), DialogTokens.kt, ScrimTokens.kt, material3 res strings.xml, compose/ui res styles.xml (vendored); Material Web 2.5.0 dialog/internal/animations.js, dialog/internal/_dialog.scss, internal/motion/animation.js (vendored); m3.material.io Dialogs (Specs, Guidelines, Accessibility; token DB md.comp.dialog, md.comp.full-screen-dialog).

DialogTokens.kt@19991f2c (generated dialog.css) as read by AlertDialog.kt, ScrimTokens.kt (scrim.css), plus components/dialog-defaults.css (PARITY.md lines):

dialog.css sources

(compose/material3 @ 19991f2c; values: dialog.css, dialog-defaults.css, scrim.css, lines in PARITY.md):

  • AlertDialog.kt BasicAlertDialog (L136-154): a dialog window, content sized in [DialogMinWidth, DialogMaxWidth], paneTitle Strings.Dialog. AlertDialogContent (L294-376): a Surface in AlertDialogDefaults.shape / containerColor with TonalElevation 0 and no shadow (owner decision M9 #8), a Column padded dialogPadding: the icon centered (IconPadding below), the title start-aligned or centered with an icon (TitlePadding below), the text weight(1, fill = false) with textPadding below, the buttons at the end (AlertDialogFlowRow L382-401: spacing 8dp, the stacked spacing less the buttons' minimum interactive size padding, which surrounds them).
  • ModalBottomSheet.kt L146-151: the scrim (ScrimTokens color at ContainerOpacity) fades on DefaultEffects (owner decision M9 #9; Compose dialogs show the platform window dim).
  • @material/web 2.5.0 dialog/internal/animations.js (secondary source; owner decision M9 #9, SOURCES register #72): open, the container slides down and its background grows from 35% of its height, then the parts fade in; close, the reverse, faster. Reduced motion: fades only. The open easing is androidx's emphasized (0.2, 0, 0, 1) (tokens/MotionTokens.kt L40, --m3e-sys-motion-easing-emphasized), not Material Web's approximation (D20 (6)). _dialog.scss: the window margin (register #73).
  • m3.material.io Dialogs › Specs (full-screen; owner decision M9 #11, register #74).