Dialogs

Summary

Dialog2 is the modal, built on the native <dialog> element: the platform provides the top layer, the ::backdrop scrim, focus containment (the page behind is inert), focus restore and Esc handling; Soma adds the look, the events and the strict-modal switch. Four widths. Purely imperative: a dialog never opens on its own; your code calls .show(). The accessible name is auto-derived from the header heading, so every dialog following the markup contract is announced by its title. Its non-modal slide-over sibling for detail views is the Drawer.

When to use

PatternUse it for
Dialog -smallConfirmations — one question, two buttons.
Soma.confirm() / Soma.alert()The everyday confirmation/acknowledgement without authoring any markup; see Confirm and alert shorthands below.
Dialog -medium+Focused create/edit forms that must complete before returning.
Dialog -large / -xlargeReviewing tabular or side-by-side content before acting: pickers, comparisons.
data-soma-modal="true"Strict must-act states (session expired) — Esc and backdrop clicks do nothing.
DrawerInspecting one item from a collection while the collection stays visible and clickable.

Examples

Small

400px — confirmations:

One question, two buttons. Esc or a backdrop click cancels.

Medium

600px — footer hint + actions:

A create form; the footer pairs a start-aligned hint with end-aligned actions.

Large

800px — tabular review:

Its markup carries no aria-labelledby; the name is auto-derived from the heading.

Xlarge

968px — side-by-side content:

The widest step; beyond this, use a full page.

Strict modal

data-soma-modal="true":

Esc and backdrop clicks do nothing; only its own button closes it.

Stacked dialogs

The top layer keeps order:

The edit dialog opens a confirm dialog on top. Esc closes only the topmost; each dialog brings its own backdrop, so the newest always dims what is below.

Events

.on('show') / .on('hide'):

Last event: (none)

Keyboard & focus

KeyAction
Tab / Shift+TabMove through the dialog's focusable elements. Containment is native: showModal() makes the rest of the page inert, so focus cannot leave the dialog — no JS focus trap involved.
EscClose the topmost open dialog (the platform's close request, surfaced as the cancel event), unless the dialog is strict (data-soma-modal="true"), which suppresses it.

Initial focus follows the platform: the first focusable element inside the dialog, or the element carrying an autofocus attribute if you set one. Put autofocus on the field the user came to fill in (a dialog with no focusable content is focused itself, so screen readers still land in it). On close (however it was triggered) the platform returns focus to the element that was focused when the dialog opened. Clicking the backdrop closes a non-strict dialog.

Top layer behaviour

An open dialog lives in the browser's top layer — above everything in the page, regardless of any z-index. Each .show() places the dialog above previously opened ones with its own ::backdrop, and Esc is routed by the platform to the topmost dialog only, so stacked dialogs unwind one at a time. The same rule covers overlays opened above the dialog: an Esc that finds an open dropdown menu or inline dialog closes that popover only, in every engine: the dialog ignores a cancel fired on the same keystroke (WebKit, which has no close-watcher stack, fires both at once). The document behind the dialog is inert: it cannot be clicked, focused, or reached by assistive technology while the dialog is open. Backdrop clicks close a non-strict dialog via the native closedby="any" light dismiss where the engine ships it; Soma covers the remaining engines (Safari) with a small fallback listener until Interop 2026 completes. Toasts also enter the top layer, so they stay visible above an open dialog.

Declarative triggers (Invoker Commands)

Because a dialog2 is a native <dialog>, the platform's Invoker Commands API (Baseline newly available, late 2025) works on it with zero JavaScript: a button pointing at the dialog with commandfor opens it modally with command="show-modal" (same top layer, backdrop and focus behaviour as .show()) and command="close" closes it:

<button class="soma-button" commandfor="dlg-invoker" command="show-modal">
  Open (declarative)</button>

<dialog class="soma-dialog2 soma-dialog2-small" id="dlg-invoker" closedby="any">
  …
  <button class="soma-button" commandfor="dlg-invoker" command="close">
    Close (declarative)</button>
</dialog>

Declarative open and close

No click handlers anywhere; both buttons are pure markup:

soma-dialog-hide events seen: 0

.show() remains the API for programmatic opening; declarative triggers are for the plain "this button opens that dialog" case. Still call Soma.dialog2('#dlg-invoker') once at load: binding derives the accessible name, wires the header close button, and attaches the native close listener, so soma-dialog-hide (and .on('hide')) fires on every close path, invoker-closed included, as the live counter above shows. One honest limitation: soma-dialog-show is dispatched only by .show() — the component does not observe native opening, so an invoker-opened dialog fires no show event. If you need an open signal from declarative triggers, listen for the dialog's native toggle event (e.newState === 'open'), which engines that ship Invoker Commands also ship. Two smaller notes: data-soma-modal="true" strictness still holds (the Esc veto is bound at init, not at show time), and backdrop light dismiss is normally arranged by .show(). For a dialog that may first open declaratively, author closedby="any" in the markup yourself, as above.

Confirm and alert shorthands

For the everyday "one question, two buttons" case you don't have to author dialog markup at all: Soma.confirm() and Soma.alert() build a small dialog2 (a native <dialog>), show it through the normal machinery (top layer, backdrop, native focus handling, derived accessible name), and remove it from the DOM after it closes. Both return a promise: confirm() resolves true on the confirm button and false on any other way out (cancel button, Esc, backdrop click); alert() resolves once dismissed. Default button labels are localised (dialog.confirm / dialog.cancel / dialog.ok in the i18n catalog), so they follow the active locale.

Confirm

Live — Promise<boolean>:

Last result: (none)

Danger confirm with callbacks

appearance: 'danger' + onConfirm/onCancel:

appearance: 'danger' tints the confirm button; the callbacks fire alongside the promise.

Alert

One OK button — Promise<void>:

Acknowledgement only: resolves however it is dismissed.

Soma.confirm(options)

OptionDescription
titleDialog title (plain text). Required; it becomes the accessible name.
bodyOptional plain-text body paragraph.
appearance'primary' (default) or 'danger', which tints the confirm button for destructive commands. Anything else throws.
confirmLabel / cancelLabelButton labels; default to the localised "Confirm" / "Cancel".
onConfirm / onCancelOptional callbacks, fired alongside the promise settling. onCancel covers the cancel button, Esc, and backdrop clicks alike.

Soma.alert(options)

OptionDescription
titleDialog title (plain text). Required.
bodyOptional plain-text body paragraph.
okLabelButton label; defaults to the localised "OK".

The promise style reads best inside async flows:

if (await Soma.confirm({
  title: 'Delete deployment?',
  body: 'This stops soma-cdn and removes its 8 instances.',
  appearance: 'danger',
  confirmLabel: 'Delete',
})) {
  await api.deleteDeployment(id);
}

await Soma.alert({ title: 'Export finished', body: '128 rows written.' });

Or use the callbacks; both styles work, on the same call:

Soma.confirm({
  title: 'Apply changes?',
  onConfirm: () => applyChanges(),
  onCancel:  () => console.log('kept as-is'),
});

HTML

The full markup contract: the element must be a native <dialog> (the factory throws otherwise). Place dialogs at the end of <body>, outside the page layout. No aria-hidden, no role, no aria-modal: a closed <dialog> is hidden by the platform, and the native open attribute is the state signal:

<dialog class="soma-dialog2 soma-dialog2-small" id="confirm">
  <header class="soma-dialog2-header">
    <h2 class="soma-dialog2-header-main">Delete?</h2>
    <button class="soma-dialog2-header-close" aria-label="Close">
      <span class="soma-icon soma-icon-close"></span>
    </button>
  </header>
  <div class="soma-dialog2-content">…</div>
  <footer class="soma-dialog2-footer">
    <div class="soma-dialog2-footer-hint">Optional start-aligned hint.</div>
    <div class="soma-dialog2-footer-actions">
      <button class="soma-button soma-button-danger">Delete</button>
      <button class="soma-button soma-button-link">Cancel</button>
    </div>
  </footer>
</dialog>

Pick a width with the size modifier; -medium (600px) is the default when no modifier is present:

<dialog class="soma-dialog2 soma-dialog2-small">…</dialog>   <!-- 400px -->
<dialog class="soma-dialog2 soma-dialog2-medium">…</dialog>  <!-- 600px -->
<dialog class="soma-dialog2 soma-dialog2-large">…</dialog>   <!-- 800px -->
<dialog class="soma-dialog2 soma-dialog2-xlarge">…</dialog>  <!-- 968px -->

A strict modal adds data-soma-modal="true": Esc and backdrop clicks do nothing, so it usually has no header close button either; only your own controls close it:

<dialog class="soma-dialog2 soma-dialog2-small" id="expired"
        data-soma-modal="true">
  <header class="soma-dialog2-header">
    <h2 class="soma-dialog2-header-main">Session expired</h2>
  </header>
  <div class="soma-dialog2-content"><p>Please sign in again.</p></div>
  <footer class="soma-dialog2-footer">
    <div class="soma-dialog2-footer-actions">
      <button class="soma-button soma-button-primary" id="relogin">Sign in</button>
    </div>
  </footer>
</dialog>

The native element carries the dialog role and modality itself, so binding only derives the accessible name. When the element has no aria-label/aria-labelledby of its own, the component points aria-labelledby at the .soma-dialog2-header-main heading (assigning it an id if needed; any h1h3 inside the header works as a fallback). An explicit attribute on the element always wins. To choose where focus lands on open, put autofocus on the element that should receive it.

CSS classes

ClassEffect
.soma-dialog2The modal card, on a native <dialog>. Hidden by the platform until open; capped at the viewport (max-height/max-width), so long content scrolls inside.
-small / -medium / -large / -xlargeWidths: 400 / 600 / 800 / 968px. No modifier = 600px.
.soma-dialog2-header / -header-main / -header-closeHeader band: the h2 heading + dismiss button. Any descendant .soma-dialog2-header-close closes on click.
.soma-dialog2-contentThe scrolling body band.
.soma-dialog2-footer / -footer-hint / -footer-actionsFooter band; the hint sits at the start, actions at the end.
[data-soma-modal="true"]Strict modal: Esc and backdrop clicks are ignored.
[open]The native open-state attribute, managed by the platform. It is the styling hook for the visible dialog (Soma's enter transition lives on it). Never write it yourself; call .show().
::backdropThe scrim behind the dialog: a platform pseudo-element in the top layer (there is no blanket element to author or query). Tinted via --soma-surface-backdrop.

JavaScript

Constructor

MemberDescription
Soma.dialog2(elOrSelector)Get or create the singleton instance for the element. Throws when nothing matches, when the element lacks the soma-dialog2 class, or when it is not a native <dialog>. Binding wires the close button and derives the accessible name (see HTML). Call it once at load so the derived name is in place before the first open.

Instance methods

MemberDescription
.show()Open via showModal(): the dialog enters the top layer with its ::backdrop, the page becomes inert, the platform moves focus in (first focusable, or your autofocus), and show is dispatched. No-op when already open.
.hide()Close via the native close(): the platform restores focus to the opener; hide is dispatched from the native close event, so it also fires when the dialog closes without .hide() (Esc, backdrop click, a method="dialog" form). No-op when already closed.
.toggle()show() or hide() depending on state.
.remove()Hide, drop the singleton, and remove the element from the DOM: teardown for dialogs built at runtime.
.destroy()Unbind and restore the pre-init markup: listeners removed, the derived aria-labelledby (and any heading id the component minted) stripped; author-supplied attributes stay, and the element stays in the DOM. Closes the dialog first if open (the final hide still fires). For fragments about to be re-rendered; use .remove() to delete the element too.
.on(event, fn) / .off(event, fn)Subscribe / unsubscribe: 'show' and 'hide'.
.isOpenBoolean state, readable at any time.

All methods return the instance, so calls chain. The underlying DOM events are bubbling CustomEvents (soma-dialog-show / soma-dialog-hide) dispatched on the dialog element, so addEventListener on the document works for delegated listening. soma-dialog-hide follows the platform's close event, which browsers fire from a queued task — expect it a moment after .hide() returns, from every close path alike.

Listen on the instance, and unsubscribe with the same function reference:

const dlg = Soma.dialog2('#confirm');

const onShow = () => console.log('opened');
dlg.on('show', onShow);
dlg.off('show', onShow);   // same fn reference removes it

Or listen delegated; the CustomEvents bubble, so one document listener sees every dialog:

document.addEventListener('soma-dialog-show', (e) => {
  console.log('dialog opened:', e.target.id);
});

Programmatic control — everything chains:

Soma.dialog2('#confirm').show();          // top layer, backdrop, focus moves in
Soma.dialog2('#confirm').hide();          // focus returns to the opener
Soma.dialog2('#confirm').toggle();
Soma.dialog2('#confirm').on('hide', refreshList).show();
if (Soma.dialog2('#confirm').isOpen) { /* … */ }

Build a dialog at runtime, bind it, and tear it down with .remove() when done:

document.body.insertAdjacentHTML('beforeend', renderedDialogMarkup);
const dlg = Soma.dialog2('#one-shot');
dlg.on('hide', () => dlg.remove());   // hide + drop instance + remove DOM
dlg.show();

Strict modal — the markup carries data-soma-modal="true"; only your own controls close it:

const expired = Soma.dialog2('#expired');
document.getElementById('relogin')
  .addEventListener('click', () => expired.hide());

The live examples above, exactly as this page wires them:

// Init at load so the auto-derived aria-labelledby is in place up front.
const confirm = Soma.dialog2('#dlg-confirm');
const form = Soma.dialog2('#dlg-form');
const large = Soma.dialog2('#dlg-large');
const xlarge = Soma.dialog2('#dlg-xlarge');
const strict = Soma.dialog2('#dlg-strict');
const stack = Soma.dialog2('#dlg-stack');

document.getElementById('open-confirm').addEventListener('click', () => confirm.show());
document.getElementById('open-form').addEventListener('click', () => form.show());
document.getElementById('open-large').addEventListener('click', () => large.show());
document.getElementById('open-xlarge').addEventListener('click', () => xlarge.show());
document.getElementById('open-strict').addEventListener('click', () => strict.show());
document.getElementById('open-stack').addEventListener('click', () => stack.show());
document.getElementById('open-events').addEventListener('click', () => confirm.show());

document.getElementById('confirm-yes').addEventListener('click', () => confirm.hide());
document.getElementById('confirm-no').addEventListener('click', () => confirm.hide());
document.getElementById('form-cancel').addEventListener('click', () => form.hide());
document.getElementById('large-close').addEventListener('click', () => large.hide());
document.getElementById('xlarge-close').addEventListener('click', () => xlarge.hide());
document.getElementById('strict-ok').addEventListener('click', () => strict.hide());

// Stacked: open the confirm on top of the edit dialog.
document.getElementById('stack-delete').addEventListener('click', () => confirm.show());
document.getElementById('stack-close').addEventListener('click', () => stack.hide());

const out = document.getElementById('dlg-events-out');
confirm.on('show', () => { out.textContent = 'soma-dialog-show'; });
confirm.on('hide', () => { out.textContent = 'soma-dialog-hide'; });

// Invoker Commands demo: no click handlers — the buttons are pure
// markup. Binding at load (above the fold: Soma.dialog2 derives the
// accessible name and attaches the native close listener) is what
// makes 'hide' fire on invoker-closed dialogs. Note there is NO
// 'show' counterpart here: soma-dialog-show only fires via .show().
const invoker = Soma.dialog2('#dlg-invoker');
let invokerHides = 0;
invoker.on('hide', () => {
  document.getElementById('invoker-out').textContent = String(++invokerHides);
});

// Shorthands — no markup anywhere on this page for these three
const scOut = document.getElementById('shorthand-out');

document.getElementById('open-shorthand-confirm').addEventListener('click', async () => {
  const ok = await Soma.confirm({
    title: 'Apply changes?',
    body: 'The service restarts within a minute.',
  });
  scOut.textContent = `resolved ${ok}`;
});

document.getElementById('open-shorthand-danger').addEventListener('click', () => {
  Soma.confirm({
    title: 'Delete deployment?',
    body: 'This stops soma-cdn and removes its 8 instances.',
    appearance: 'danger',
    confirmLabel: 'Delete',
    onConfirm: () => { scOut.textContent = 'onConfirm fired'; },
    onCancel: () => { scOut.textContent = 'onCancel fired'; },
  });
});

document.getElementById('open-shorthand-alert').addEventListener('click', async () => {
  await Soma.alert({
    title: 'Export finished',
    body: '128 rows written to soma-export.csv.',
  });
  scOut.textContent = 'alert resolved';
});

Delete deployment?

This stops soma-cdn and removes its 8 instances. It cannot be undone.

New environment

Recent deployments

ServiceVersionWhenResult
api-backendv2.3.1Today 09:14success
cortex-gatewayv1.9.0Yesterday 16:02failed
axon-runnerv0.12.4Mon 11:40success

Compare environments

Settingstaging-euproduction-euSettingstaging-usproduction-us
Replicas13Replicas14
Regionus-eastus-eastRegionus-eastus-east
AutoscaleoffonAutoscaleoffon

Session expired

Strict modal: Esc and backdrop clicks do nothing — only the button below closes it.

Declaratively opened

No script opened this dialog — the trigger carries commandfor="dlg-invoker" command="show-modal". The button below closes it with command="close"; Esc and the backdrop work too.

Edit service

The Delete button opens the confirm dialog on top of this one.