v1.0.0

<bmx-dialog>

A modal window: the confirmation before something irreversible, the form that does not deserve a page of its own, the detail view over a table. It takes the page's whole attention until it is answered, which is the reason to reach for one and the reason not to reach for one lightly.

11 properties · 2 events · 2 methods · 7 parts

Example

Open the dialog Escape, the backdrop and the close button all dismiss it, and each reports a different reason.

Choose a name your team will recognise. You can change it again at any time.

Cancel Save
Delete the workspace Nothing has happened yet.

Everything in it goes with it, and it cannot be undone.

Keep it Delete
Show markup
<div class="row">
  <bmx-button id="ex-dialog-open">Open the dialog</bmx-button>
  <span class="note">Escape, the backdrop and the close button all dismiss it, and each reports a different reason.</span>
</div>

<bmx-dialog id="ex-dialog" heading="Rename this project">
  <p style="margin: 0 0 var(--bmx-space-3)">Choose a name your team will recognise. You can change it again at any time.</p>
  <bmx-input id="ex-dialog-name" label="Project name" value="Northwind migration" full-width></bmx-input>

  <bmx-button slot="footer" variant="ghost" tone="neutral" id="ex-dialog-cancel">Cancel</bmx-button>
  <bmx-button slot="footer" id="ex-dialog-save">Save</bmx-button>
</bmx-dialog>

<div class="row">
  <bmx-button id="ex-dialog-confirm-open" variant="outline" tone="danger">Delete the workspace</bmx-button>
  <span class="note" id="ex-dialog-out">Nothing has happened yet.</span>
</div>

<!--
  A confirmation that has to be answered rather than escaped: no close button,
  no Escape, no backdrop. `closeDialog()` still works, which is what the two
  buttons below use - the restriction is on the user, not on your application.
-->
<bmx-dialog id="ex-dialog-confirm" heading="Delete this workspace?" size="sm" dismissible="false">
  <p style="margin: 0">Everything in it goes with it, and it cannot be undone.</p>

  <bmx-button slot="footer" variant="ghost" tone="neutral" id="ex-dialog-no">Keep it</bmx-button>
  <bmx-button slot="footer" tone="danger" id="ex-dialog-yes">Delete</bmx-button>
</bmx-dialog>

<script type="module">
  await customElements.whenDefined('bmx-dialog');

  const dialog = document.getElementById('ex-dialog');

  document.getElementById('ex-dialog-open').addEventListener('bmxActivate', () => dialog.openDialog());
  document.getElementById('ex-dialog-cancel').addEventListener('bmxActivate', () => dialog.closeDialog());
  document.getElementById('ex-dialog-save').addEventListener('bmxActivate', () => dialog.closeDialog());

  const confirm = document.getElementById('ex-dialog-confirm');
  const readout = document.getElementById('ex-dialog-out');

  document.getElementById('ex-dialog-confirm-open').addEventListener('bmxActivate', () => confirm.openDialog());
  document.getElementById('ex-dialog-no').addEventListener('bmxActivate', () => confirm.closeDialog());
  document.getElementById('ex-dialog-yes').addEventListener('bmxActivate', () => confirm.closeDialog());

  // `bmxClose` arrives once the dialog has finished closing, and says why. The
  // difference matters to anything that autosaves: a form abandoned by Escape
  // and one closed by its own Cancel button are the same event to the DOM.
  confirm.addEventListener('bmxClose', event => {
    readout.textContent = `Closed by: ${event.detail.reason}.`;
  });
</script>

WHAT IT IS BUILT ON, AND WHY THAT MATTERS TO YOU

A native <dialog> opened with showModal(). Three things come free with that and they are the three that hand-built modals get wrong: the panel is in the browser's top layer, so it is above every other stacking context without a z-index arms race and above a menu that opened it; everything outside is genuinely inert, which traps focus, the pointer and a screen reader's own cursor rather than only the Tab key; and Escape is the browser's, so it behaves the way the rest of the operating system does.

Three things do not come free, and this component is those three: the page behind a modal dialog still scrolls to the wheel; close() removes the panel in the same frame, so an exit animation never draws; and focus goes back where it came from only sometimes, and never into a BMX control's shadow root. See dialog-surface.ts.

NAMING IT

Give it a heading, or a label when the design has no visible title. A modal with neither is announced as "dialog" and nothing else, which is the commonest accessibility defect in the whole pattern - and the one that is invisible to everybody who is not using a screen reader.

FORMS

A <form method="dialog"> inside the content closes the dialog, because that is the platform's own way of writing a dialog's buttons with no script at all, and it is reported with a reason of form so an application can tell it apart from a dismissal. It is honoured by this component rather than by the browser: your form arrives through a slot, which makes it a child of this element and not a descendant of the <dialog> inside it, and the platform's rule looks for a dialog ancestor. Written by hand it would do nothing at all - see dialog-surface.ts.

Properties

PropertyAttributeTypeDefaultDescription
closeLabel close-label string 'Close' The close button's accessible name.
closeOnBackdrop close-on-backdrop boolean true Whether a press on the scrim dismisses it.
closeOnEscape close-on-escape boolean true Whether Escape dismisses it.
dismissible dismissible boolean true Whether the user may dismiss it. False removes the close button and refuses both Escape and the backdrop - for a dialog that must be answered rather than escaped. It does not restrain the application: closeDialog() always works, and a modal that survived its own submit button would be a support call.
heading heading string The visible title. Also the dialog's accessible name.
hideClose hide-close boolean false Hide the close button while still allowing Escape and the backdrop.
initialFocus initial-focus string | HTMLElement What is focused when it opens, as a CSS selector or an element. Left alone, focus goes to the panel, so the dialog is announced from its name and its content rather than from whichever control happens to be first. Name something here when a particular field is obviously the point of the dialog - and note that an autofocus attribute on your own content wins over both, because that attribute is exactly what it is for.
label label string The accessible name, for a design with no visible title.
open open boolean false Whether the dialog is showing. Mutable, so the component can close itself when the user dismisses it. A consumer driving it may treat it as controlled and watch bmxOpenChange.
returnFocus return-focus boolean true Whether closing returns focus to whatever opened it.
size size BmxDialogSize 'md' The panel's width step. full fills the viewport.

Events

EventDetailDescription
bmxClose BmxDialogCloseDetail Fired once it has finished closing, with why it closed.
bmxOpenChange boolean Fired when the dialog opens or closes.

Methods

MethodSignatureDescription
closeDialog closeDialog() => Promise<void> Close the dialog. Always permitted, whatever dismissible says - that property restrains the user, not the application.
openDialog openDialog() => Promise<void> Open the dialog.

Slots

SlotDescription
(default) The dialog's content.
footer The row of actions along the bottom.
heading Replaces the plain-text heading.

CSS shadow parts

PartDescription
body The scrolling content area.
close The close button.
dialog The native element, which is also the scrim.
footer The row of actions.
header The bar across the top.
heading The title within it.
panel The box the content sits in.

CSS custom properties

PropertyDescription
--bmx-dialog-inset Space kept between the panel and the viewport's edges.
--bmx-dialog-padding Space inside the panel's header, body and footer.
--bmx-dialog-radius Corner radius of the panel.
--bmx-dialog-scrim The colour laid over the page behind it.
--bmx-dialog-width The panel's width at the current size step.