<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
Choose a name your team will recognise. You can change it again at any time.
Everything in it goes with it, and it cannot be undone.
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
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
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
| Event | Detail | Description |
|---|---|---|
bmxClose |
BmxDialogCloseDetail |
Fired once it has finished closing, with why it closed. |
bmxOpenChange |
boolean |
Fired when the dialog opens or closes. |
Methods
| Method | Signature | Description |
|---|---|---|
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
| Slot | Description |
|---|---|
(default) |
The dialog's content. |
footer |
The row of actions along the bottom. |
heading |
Replaces the plain-text heading. |
CSS shadow parts
| Part | Description |
|---|---|
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
| Property | Description |
|---|---|
--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. |