xoong
Menu · Dialogs and modals
DocsOverlaysDialogs and modals

Dialogs and modals

The common questions are built in: confirm, alert, prompt and ask, plus a titled modal, <xo-dialog heading>. They are the browser's own <dialog>: the page behind is inert, the focus stays inside, Escape answers "no". Import @xoong/ui and they work; there is nothing to configure.

Which one to use

You want toUseBlocks the page
Ask yes or no before something that cannot be undoneconfirm(), or xo-confirm on a buttonYes
Tell the person something they must see (info, success, warning, error)alert()Yes
Ask for one line of textprompt()Yes
Offer two or three named choices (Save, Don't save, Keep editing)ask()Yes
Show a form or any content in a window<xo-dialog heading>Yes
Say "done" without asking anything, maybe with UndoA toast: xo-notice or toast.show()No
Show a small panel next to a button<xo-popover>No
List actions for one item<xo-menu>No
Explain an icon button on hover or focusxo-tooltipNo

Rule of thumb: block the page only when the person must decide or must see it. Everything that can be undone is better as a toast with Undo than as a question.

Confirmation

confirm(message, options) resolves to true or false. The focus starts on Cancel, the safe choice. With no kind the confirming button is red, as for a delete. A kind of warning, info or success adds an icon and colours the button to match; title adds a heading and detail a second, quieter line.

Answer: nothing yet

<p class="toolbar">
  <button data-demo="confirm-plain">Plain</button>
  <button data-demo="confirm-danger">Danger</button>
  <button data-demo="confirm-warning">Warning</button>
  <button data-demo="confirm-info">Info</button>
</p>
<p class="hint">Answer: <output id="out-confirm">nothing yet</output></p>
import { confirm } from '@xoong/ui';

if (await confirm('The 14 articles in it are deleted too.', {
  kind: 'danger',          // 'danger' | 'warning' | 'info' | 'success'
  title: 'Delete project?',
  ok: 'Delete',            // the text of the confirming button
  cancel: 'Keep it',
  detail: 'You can restore it for 30 days.',
})) {
  // go on
}

On a button that runs an operation, the same question is an attribute. The extra attributes tune the dialog; the button is red unless xo-confirm-kind says otherwise.

<button xo-click="deletePerson" xo-confirm="Delete {name}? Their posts stay."
        xo-confirm-title="Delete person" xo-confirm-ok="Delete" xo-confirm-kind="danger" xo-notice="Deleted">Delete</button>

<button xo-click="publishPost" xo-confirm="Subscribers get an e-mail." xo-confirm-kind="info"
        xo-confirm-title="Publish now?" xo-confirm-ok="Publish">Publish</button>

Alert: info, success, warning, error

alert(message, options) shows one OK button and resolves when it is closed. kind defaults to info; use success, warning or danger (also written error). The focus starts on OK. An alert is announced as an alertdialog with the title as its name and the message as its description.

Result: nothing yet

<p class="toolbar">
  <button data-demo="alert-info">Info</button>
  <button data-demo="alert-success">Success</button>
  <button data-demo="alert-warning">Warning</button>
  <button data-demo="alert-error">Error</button>
</p>
<p class="hint">Result: <output id="out-alert">nothing yet</output></p>
import { alert } from '@xoong/ui';

await alert('We e-mail you when the file is ready.', { title: 'Export started' });                  // info
await alert('Your changes are live.', { kind: 'success', title: 'Published' });
await alert('Two of the five images have no description.', { kind: 'warning', title: 'Saved with warnings' });
await alert('The file is larger than 10 MB.', { kind: 'error', title: 'Upload failed', detail: 'Choose a smaller file.' });

Prompt

prompt(message, options) asks for a line of text: it resolves with the text, or null when cancelled. The field is a native input, so type, required and pattern are checked by the browser, Enter submits, and an empty required field keeps the dialog open and marks the field invalid.

Answer: nothing yet

<p class="toolbar">
  <button data-demo="prompt">Name a view</button>
  <button data-demo="prompt-required">Required web address</button>
</p>
<p class="hint">Answer: <output id="out-prompt">nothing yet</output></p>
import { prompt } from '@xoong/ui';

const name = await prompt('Name this view', { title: 'Save view', label: 'Name', value: 'My view' });
if (name !== null) save(name);                       // null means cancelled

const href = await prompt('Where should the link point?', { label: 'Address', type: 'url', required: true });

Choices

ask(message, { buttons }) offers any set of choices and resolves with the value of the one pressed. Mark one button primary (the filled one), one danger if it destroys something, and one cancel: Escape answers with its value, and the focus starts on it. Without a cancel button, Escape resolves null.

Answer: nothing yet

<p class="toolbar"><button data-demo="ask">Leave with unsaved changes</button></p>
<p class="hint">Answer: <output id="out-ask">nothing yet</output></p>
import { ask } from '@xoong/ui';

const answer = await ask('You have unsaved changes.', {
  kind: 'warning',
  title: 'Leave this page?',
  buttons: [
    { label: "Don't save", value: 'discard', danger: true },
    { label: 'Keep editing', value: 'stay', cancel: true },
    { label: 'Save and leave', value: 'save', primary: true },
  ],
});
// 'discard' | 'stay' | 'save'

A titled modal

Give <xo-dialog> a heading and it becomes a window: a header with the title and a Close button, the content as the body, and anything marked slot="footer" as the footer. size is sm, the default, wide or full; on a phone it fills the screen. dismiss closes it on a click outside it, which is right for a harmless dialog and wrong for a form with unsaved input, so it is off by default.

A footer button outside the form still submits it with form="id"; with method="dialog" the form closes the dialog and the button's value is the answer.

Closed by: nothing yet

Press Ctrl + K for the command palette.

Wide dialogs suit tables and side-by-side content. The body scrolls when it is taller than the window; the header and footer stay put.

<p class="toolbar">
  <button xo-open="#d-edit">Edit person</button>
  <button xo-open="#d-small">Small, click outside closes</button>
  <button xo-open="#d-wide">Wide</button>
</p>
<p class="hint">Closed by: <output id="out-modal">nothing yet</output></p>

<xo-dialog id="d-edit" heading="Edit person" data-out="out-modal">
  <form method="dialog" id="edit-form" class="xo-fields">
    <label>Name <input name="name" value="Ada Lovelace" autocomplete="off"></label>
    <label>E-mail <input type="email" name="email" value="ada@example.org" autocomplete="off"></label>
  </form>
  <button slot="footer" type="button" xo-close>Cancel</button>
  <button slot="footer" type="submit" form="edit-form" value="saved" class="primary">Save</button>
</xo-dialog>

<xo-dialog id="d-small" heading="Keyboard shortcuts" size="sm" dismiss data-out="out-modal">
  <p>Press <kbd>Ctrl</kbd> + <kbd>K</kbd> for the command palette.</p>
  <button slot="footer" type="button" xo-close value="got it" class="primary">Got it</button>
</xo-dialog>

<xo-dialog id="d-wide" heading="Release notes" size="wide" data-out="out-modal">
  <p>Wide dialogs suit tables and side-by-side content. The body scrolls when it is taller than the window; the header and footer stay put.</p>
  <button slot="footer" type="button" xo-close>Close</button>
</xo-dialog>
// xo-open="#id" opens it from markup; from JavaScript, open() resolves with the answer
const answer = await document.querySelector('#d-edit').open();   // 'saved' | '' (Escape, Cancel, Close)

A form made by xoong goes in the same way: put <xo-form op="createPerson"> in the body and the dialog closes by itself when the form succeeds (xo-done). <xo-dialog> without a heading stays a plain wrapper around whatever you put in it, as before.

Notices that need no answer

If nothing has to be decided, do not block the page. xo-notice="Saved" on a button or form raises a toast after success; from JavaScript use toast.show(), with an action such as Undo.

<p class="toolbar"><button data-demo="toast">Move to the trash</button></p>
import { toast } from '@xoong/ui';

toast.show({ message: 'Article moved to the trash', action: { label: 'Undo', run: () => restore() } });

A failed action can offer the same kind of button. Listen for xo-error and set event.detail.action = { label: 'Retry', run } (and event.detail.timeout when the toast should stay a different length) before the toast draws it, one microtask later.

Behaviour and accessibility

Texts and languages

Every built-in text is a catalog key (xoong.confirm.ok, xoong.confirm.cancel, xoong.dialog.ok, xoong.dialog.close), Vietnamese included. For one call pass ok and cancel; for the whole app set confirmLabels.ok = 'Delete'.

Your own dialog

To draw the question yourself, for xo-confirm and for the leave-this-page question of a dirty form, install a replacement; resolve to true to go on.

import { setConfirm } from '@xoong/ui';

setConfirm(async (message, context) => {
  // context: { kind: 'action', element } for xo-confirm, { kind: 'leave', forms } for the leave guard
  return myDialog(message);
});

In tests

Happy-dom has no showModal(); xoong then sets the open attribute instead, so a test finds dialog.xo-confirm, reads its text and clicks its buttons like any other markup: screen.$('dialog.xo-confirm .xo-confirm-ok').click().