Dialogs
The browser's own modal <dialog>: opened by a command, the page behind it inert and dimmed, closed by its buttons, Esc or a click outside. And REDZ.ui.confirm() for a question asked from script.
Examples
A button opens the dialog with commandfor + command="show-modal"; buttons inside close it with command="close". closedby="any" lets a click outside close it too. The focus goes into the dialog - to the control marked autofocus - and back to the button when it closes.
<button class="btn" type="button" commandfor="dialog-edit-note" command="show-modal"><i class="bi bi-chat-left-text" aria-hidden="true"></i>Add a note</button>
<dialog id="dialog-edit-note" class="dialog" closedby="any" aria-labelledby="dialog-edit-note-title">
<div class="dialog-header">
<h2 class="dialog-title" id="dialog-edit-note-title">A note for the guides</h2>
<button class="btn-close" type="button" commandfor="dialog-edit-note" command="close" aria-label="Close"></button>
</div>
<div class="dialog-body vstack gap-8">
<label class="form-field"><span class="form-label">Note</span><textarea class="form-control" autofocus>Pick up at the pier at 8:30.</textarea></label>
</div>
<div class="dialog-footer">
<button class="btn" type="button" commandfor="dialog-edit-note" command="close">Cancel</button>
<button class="btn btn-primary" type="button" commandfor="dialog-edit-note" command="close">Save</button>
</div>
</dialog>A product tints the head of every dialog with --redz-dialog-header-bg; --redz-dialog-header-end gives it room below and --redz-dialog-header-line a line under it.
Sizes
dialog-sm for a question, the default for a form, dialog-lg for a wide table or a preview. A dialog is a region, so the width prefixes inside it measure the dialog: sm: fires in the default one, and md: in dialog-lg, 800 px wide.
<button class="btn" type="button" commandfor="dialog-sizes-sm" command="show-modal">Small</button>
<button class="btn" type="button" commandfor="dialog-sizes-lg" command="show-modal">Large</button>
<dialog id="dialog-sizes-sm" class="dialog dialog-sm" aria-labelledby="dialog-sizes-sm-title">
<div class="dialog-header"><h2 class="dialog-title" id="dialog-sizes-sm-title">Publish now?</h2></div>
<div class="dialog-body"><p>The page goes live on three sites.</p></div>
<div class="dialog-footer">
<button class="btn" type="button" commandfor="dialog-sizes-sm" command="close" autofocus>Cancel</button>
<button class="btn btn-primary" type="button" commandfor="dialog-sizes-sm" command="close">Publish</button>
</div>
</dialog>
<dialog id="dialog-sizes-lg" class="dialog dialog-lg" closedby="any" aria-labelledby="dialog-sizes-lg-title">
<div class="dialog-header">
<h2 class="dialog-title" id="dialog-sizes-lg-title">History of changes</h2>
<button class="btn-close" type="button" commandfor="dialog-sizes-lg" command="close" aria-label="Close"></button>
</div>
<div class="dialog-body"><p>Edited yesterday by M. K.; published on three sites; the Thai translation updated today.</p></div>
</dialog>A confirmation from script
await REDZ.ui.confirm({title, text, ok, tone}) builds an alertdialog, waits for the answer and removes it: true for the confirming button, false for Cancel, Esc or a click outside. The focus starts on Cancel; the button words come from the caller or the page's texts.
<button class="btn btn-danger" type="button" data-docs-run="confirm"><i class="bi bi-trash3" aria-hidden="true"></i>Delete 2 excursions</button>
<span class="text-muted fs-12" role="status" data-docs-answer></span>For what cannot be undone, phrase asks the person to type a word first - the site's name, say: a field under the text, labelled by the page's text confirm.phrase, takes the focus, the confirming button waits until the words match, and Enter in the field confirms then.
<button class="btn btn-danger" type="button" data-docs-run="confirm-phrase"><i class="bi bi-trash3" aria-hidden="true"></i>Delete the site</button>
<span class="text-muted fs-12" role="status" data-docs-answer></span>What a deletion takes with it is said by note and sections: note: {tone, text} is an alert under the text - warning by default, danger, info or success - and each of sections: [{title, items}] a title over a list, such as what goes and what stays. Every word is text, never HTML.
<button class="btn btn-danger" type="button" data-docs-run="confirm-lists"><i class="bi bi-trash3" aria-hidden="true"></i>Delete the project</button>
<span class="text-muted fs-12" role="status" data-docs-answer></span>await REDZ.ui.confirm({
title: 'Delete the project Riverside tower?',
note: {tone: 'danger', text: 'This cannot be undone.'},
sections: [
{title: 'Will be deleted:', items: ['128 tasks', '342 files', '1,204 log entries']},
{title: 'Kept:', items: ['The audit history']},
],
ok: 'Delete', tone: 'danger', phrase: 'Riverside tower',
});
A question for one value
await REDZ.ui.prompt({title, text, label, value, placeholder, type, inputmode, pattern, required, ok, cancel, tone}) asks for one value in a modal dialog and answers it, or null for Cancel and Esc. The field takes the focus with its text selected; Enter answers once the field is valid - required, pattern and type are held by the browser's own checks. The field is labelled by label, or the title; the buttons' words come from the caller or the page's texts.
<button class="btn" type="button" data-docs-run="prompt"><i class="bi bi-pencil" aria-hidden="true"></i>Rename the folder</button>
<span class="text-muted fs-12" role="status" data-docs-answer></span>const name = await REDZ.ui.prompt({title: 'Rename the folder', label: 'Name', value: 'Tours 2026', required: true, ok: 'Rename'});
if (name !== null) {
// send it
}
On a phone and full screen
On any window a long dialog keeps its header and its footer in view: only dialog-body scrolls between them. A <form> may hold all three - <form method="dialog"> right inside the dialog, its submit buttons in the footer - and the body still scrolls alone. On a window narrower than 576 px a dialog fills the screen by itself, while dialog-sm and a confirmation stay a card in the middle. dialog-fullscreen fills the screen on any window.
<button class="btn" type="button" commandfor="dialog-full-panel" command="show-modal"><i class="bi bi-arrows-fullscreen" aria-hidden="true"></i>Open full screen</button>
<dialog class="dialog dialog-fullscreen" id="dialog-full-panel" aria-labelledby="dialog-full-title">
<div class="dialog-header">
<h2 class="dialog-title" id="dialog-full-title">Route of the day</h2>
<button class="btn-close" type="button" commandfor="dialog-full-panel" command="close" aria-label="Close"></button>
</div>
<div class="dialog-body"><p>Pick-up at the hotel at 7:30, the pier at 8:15, North Bay at 9:30; snorkeling, the emerald lake, lunch on the boat, back by 17:00.</p></div>
<div class="dialog-footer"><button class="btn btn-primary" type="button" commandfor="dialog-full-panel" command="close">Done</button></div>
</dialog>Keyboard and accessibility
- Tab stays inside an open dialog; Esc closes it; the focus goes back to the button that opened it.
- The dialog is named by its title through
aria-labelledby. A question that must be answered isrole="alertdialog"with its focus on the safe answer. - The page behind does not scroll while a dialog is open, and keeps its width: the scrollbar's place stays.
- A notice raised while a dialog is open shows inside the dialog, since everything outside it is inert.
- Where the browser lacks invoker commands (below Chrome 135, Firefox 144, Safari 26.2) or
closedby(below Chrome 134, Firefox 141, and in Safari), the library's script stands in.
Tokens
From css/components/dialog.css, which declares no custom property of its own.
It reads the theme's --redz-border, --redz-radius-16, --redz-bg-raised, --redz-shadow-3, --redz-space-12, --redz-space-16, --redz-space-24, --redz-dialog-header-end, --redz-dialog-header-line, --redz-dialog-header-bg, --redz-fs-16, --redz-text-muted, --redz-space-8, --redz-bg-page, --redz-ease.
From Bootstrap 5.3
| Bootstrap 5.3 | REDZ.UI | Change | Note |
|---|---|---|---|
modal | dialog | rename | on a <dialog>, opened by commandfor + command="show-modal" |
modal-backdrop | - | remove | the browser's ::backdrop |
modal-body | dialog-body | rename | |
modal-content | - | remove | no wrappers: the <dialog> is the box |
modal-dialog | - | remove | no wrappers: the <dialog> is the box |
modal-dialog-centered | - | remove | a dialog is centered |
modal-dialog-scrollable | - | remove | the dialog's body scrolls by itself |
modal-footer | dialog-footer | rename | |
modal-fullscreen | dialog-fullscreen | rename | |
modal-header | dialog-header | rename | |
modal-static | - | remove | closedby="closerequest" |
modal-title | dialog-title | rename | |
data-bs-toggle="modal" | commandfor="ID" command="show-modal" | trigger | on the button; the dialog is a <dialog id="ID"> |
data-bs-dismiss="modal" | commandfor="ID" command="close" | trigger | |
data-bs-backdrop="static" | closedby="closerequest" | trigger | Esc closes it, a click outside does not |
data-bs-keyboard="false" | closedby="none" | trigger | only its buttons close it |
bootstrap.Modal | dialog.showModal() and dialog.close() | trigger | |
show.bs.modal | toggle | review | the dialog's toggle event, or code right after showModal() |
shown.bs.modal | toggle | review | the dialog's toggle event, or code right after showModal() |
hide.bs.modal | cancel | review | the dialog's cancel event (Esc); your buttons close it |
hidden.bs.modal | close | rename | the dialog's close event; dialog.returnValue holds the closing button's value |