Dialog
Dialogs, sometimes called "modals", appear above the page and require the user's immediate attention.
<div class="flex justify-center">
<sa-button variant="ButtonVariant.Outline" commandfor="--dialog-intro" command="show-modal">
Open
</sa-button>
</div>
<sa-dialog id="--dialog-intro">
<sa-dialog-header>
<sa-dialog-title>Edit traveller</sa-dialog-title>
<sa-dialog-description>Update the traveller's details here. Click save when you're done.</sa-dialog-description>
</sa-dialog-header>
<sa-field-group>
<sa-field>
<sa-label for="--dialog-intro-name">Name</sa-label>
<sa-input id="--dialog-intro-name" name="name" value="Ibn Battuta"/>
</sa-field>
<sa-field>
<sa-label for="--dialog-intro-username">Username</sa-label>
<sa-input id="--dialog-intro-username" name="username" value="@@ibnbattuta"/>
</sa-field>
</sa-field-group>
<sa-dialog-footer>
<sa-button variant="ButtonVariant.Outline" commandfor="--dialog-intro" command="close">
Cancel
</sa-button>
<sa-button commandfor="--dialog-intro" command="close">
Save Changes
</sa-button>
</sa-dialog-footer>
</sa-dialog>Usage
<sa-dialog> renders a standard HTML dialog element. Use the Invoker Commands API to open and close it by adding commandfor and command attributes to a <sa-button> element.
<sa-button commandfor="--custom-dialog" command="show-modal">
Dialog Trigger
</sa-button>
<sa-dialog id="--custom-dialog">
<sa-dialog-header>
<sa-dialog-title>...</sa-dialog-title>
<sa-dialog-description>...</sa-dialog-description>
</sa-dialog-header>
<!-- Dialog content goes here -->
<sa-dialog-footer>
<sa-button variant="ButtonVariant.Outline" commandfor="--custom-dialog" command="close">
Close
</sa-button>
</sa-dialog-footer>
</sa-dialog>For more information, you can review the following MDN documentation:
- <dialog>: The Dialog element
- HTMLDialogElement (documents the JS API)
- Invoker Commands API
Examples
Dismissing the dialog
Control how the dialog can be dismissed using the closedby attribute. The default behavior allows closing via the Esc key or a close button. Set closedby="any" to also allow clicking the backdrop (light dismiss), or closedby="none" to require explicit dismissal via a close button only.
<sa-group justify="GroupJustify.Center">
<sa-button variant="ButtonVariant.Outline" commandfor="--dialog-dismiss-default" command="show-modal">
Default Dismiss
</sa-button>
<sa-button variant="ButtonVariant.Outline" commandfor="--dialog-dismiss-light" command="show-modal">
Light Dismiss
</sa-button>
<sa-button variant="ButtonVariant.Outline" commandfor="--dialog-dismiss-manual" command="show-modal">
Manual Dismiss
</sa-button>
</sa-group>
<sa-dialog id="--dialog-dismiss-default">
<sa-dialog-header>
<sa-dialog-title>Default Dismiss</sa-dialog-title>
</sa-dialog-header>
<p>This dialog can be closed by pressing the
<sa-kbd>Esc</sa-kbd>
key or clicking the close button below.
</p>
<sa-dialog-footer>
<sa-button variant="ButtonVariant.Outline" commandfor="--dialog-dismiss-default" command="close">
Close
</sa-button>
</sa-dialog-footer>
</sa-dialog>
<sa-dialog id="--dialog-dismiss-light" closedby="any">
<sa-dialog-header>
<sa-dialog-title>Light Dismiss</sa-dialog-title>
</sa-dialog-header>
<p>This dialog can be closed by pressing the
<sa-kbd>Esc</sa-kbd>
key, clicking on the backdrop, or clicking the close button below.
</p>
<sa-dialog-footer>
<sa-button variant="ButtonVariant.Outline" commandfor="--dialog-dismiss-light" command="close">
Close
</sa-button>
</sa-dialog-footer>
</sa-dialog>
<sa-dialog id="--dialog-dismiss-manual" closedby="none">
<sa-dialog-header>
<sa-dialog-title>Manual Dismiss</sa-dialog-title>
</sa-dialog-header>
<p>This dialog can only be closed by clicking the close button below.
</p>
<sa-dialog-footer>
<sa-button variant="ButtonVariant.Outline" commandfor="--dialog-dismiss-manual" command="close">
Close
</sa-button>
</sa-dialog-footer>
</sa-dialog>Set return value
Add submit buttons to a form with method="dialog" and set a value on each button. When the buttons are clicked, the dialog will close and the returnValue will be set to the value of the button that was clicked.
If you want to return a result from the dialog, we strongly recommend using the dialog() helper which allows you to use dialogs in an async/await style
<div class="flex justify-center">
<sa-button variant="ButtonVariant.Outline" commandfor="--dialog-return-value" command="show-modal">
Open Alert Dialog
</sa-button>
</div>
<sa-dialog id="--dialog-return-value">
<sa-dialog-header>
<sa-dialog-title>Are you absolutely sure?</sa-dialog-title>
<sa-dialog-description>
This action cannot be undone. This will permanently cancel booking VT-48213 and release the reserved seats.
</sa-dialog-description>
</sa-dialog-header>
<form method="dialog">
<sa-dialog-footer>
<sa-button variant="ButtonVariant.Outline" type="submit" value="cancel">
Cancel
</sa-button>
<sa-button type="submit" value="confirm" autofocus>
Continue
</sa-button>
</sa-dialog-footer>
</form>
</sa-dialog>
<script>
(function() {
const dialog = document.getElementById("--dialog-return-value");
dialog.addEventListener("close", () => {
const cancelled = dialog.returnValue === "" || dialog.returnValue === "cancel";
if (cancelled) {
alert("The action has been cancelled");
return;
}
alert("The action has been confirmed");
});
dialog.addEventListener("toggle", (e) => {
// Reset the return value every time the dialog opens to prevent a previous
// returnValue from being returned when pressing the Esc key
if (e.newState === "open") {
dialog.returnValue = "";
}
});
})();
</script>Get form content depending on return value
Check returnValue to determine which button was clicked, then use FormData in the close event handler to read the submitted values.
If you want to return a result from the dialog, we strongly recommend using the dialog() helper which allows you to use dialogs in an async/await style
<sa-stack align="StackAlign.Start" class="min-w-md">
<sa-button variant="ButtonVariant.Outline" commandfor="--dialog-return-form-value-dialog" command="show-modal">
Open Dialog with Form
</sa-button>
<label class="text-sm font-bold">Output:</label>
<div class="font-mono w-full h-40 overflow-y-auto rounded-md border border-border bg-muted p-3 text-foreground" id="--dialog-return-form-value-output">-</div>
</sa-stack>
<sa-dialog id="--dialog-return-form-value-dialog">
<form method="dialog" class="grid gap-4">
<sa-dialog-header>
<sa-dialog-title>Edit traveller</sa-dialog-title>
<sa-dialog-description>Update the traveller's details here. Click save when you're done.
</sa-dialog-description>
</sa-dialog-header>
<sa-field-group>
<sa-field>
<sa-label for="--dialog-return-form-value-name">Name</sa-label>
<sa-input id="--dialog-return-form-value-name" name="name" value="Ibn Battuta" required/>
</sa-field>
<sa-field>
<sa-label for="--dialog-return-form-value-username">Username</sa-label>
<sa-input id="--dialog-return-form-value-username" name="username" value="@@ibnbattuta"
required/>
</sa-field>
</sa-field-group>
<sa-dialog-footer>
<sa-button variant="ButtonVariant.Outline" type="button" commandfor="--dialog-return-form-value-dialog"
command="close">
Cancel
</sa-button>
<sa-button type="submit" value="confirm">
Save Changes
</sa-button>
</sa-dialog-footer>
</form>
</sa-dialog>
<script>
(function() {
const dialog = document.getElementById("--dialog-return-form-value-dialog");
const output = document.getElementById("--dialog-return-form-value-output");
dialog.addEventListener("close", () => {
const cancelled = dialog.returnValue === "" || dialog.returnValue === "cancel";
if (cancelled) {
output.innerHTML = "Cancelled";
return;
}
const form = dialog.querySelector("form");
const data = Object.fromEntries(new FormData(form));
output.innerHTML = JSON.stringify(data, null, 2);
});
dialog.addEventListener("toggle", (e) => {
// Reset the return value every time the dialog opens to prevent a previous
// returnValue from being returned when pressing the Esc key
if (e.newState === "open") {
dialog.returnValue = "";
}
});
})();
</script>Scrollable content
Wrap long content in a container with a fixed height and vertical scrolling enabled (using the Tailwind max-h- and overflow-y-auto utility classes) to make it scrollable within the dialog without resizing the dialog itself.
<div class="flex justify-center">
<sa-button variant="ButtonVariant.Outline" commandfor="--dialog-scrollable-content" command="show-modal">
Open
</sa-button>
</div>
<sa-dialog id="--dialog-scrollable-content">
<sa-dialog-header>
<sa-dialog-title>Scrollable Content</sa-dialog-title>
<sa-dialog-description>
This is a dialog with scrollable content.
</sa-dialog-description>
</sa-dialog-header>
<div class="-mx-4 max-h-[300px] overflow-y-auto px-4">
@for (int i = 0; i < 10; i++)
{
<p class="mb-4 leading-normal">
Bookings are confirmed once full payment has been received. Fares are quoted per
traveller in the currency shown at checkout and include all mandatory taxes and
carrier surcharges known at the time of booking. Changes to travel dates, routing
or traveller names are subject to the fare rules of the airline and may attract a
change fee. Refunds, where permitted, are processed to the original form of payment
within fourteen days of the request being approved.
</p>
}
</div>
</sa-dialog>Sticky footer
Combine scrollable content with <sa-dialog-footer> to keep the footer always visible while the body scrolls.
<div class="flex justify-center">
<sa-button variant="ButtonVariant.Outline" commandfor="--dialog-sticky-footer" command="show-modal">
Open
</sa-button>
</div>
<sa-dialog id="--dialog-sticky-footer">
<sa-dialog-header>
<sa-dialog-title>Sticky Footer</sa-dialog-title>
<sa-dialog-description>
This dialog has a sticky footer that stays visible while the content
scrolls.
</sa-dialog-description>
</sa-dialog-header>
<div class="-mx-4 max-h-[300px] overflow-y-auto px-4">
@for (int i = 0; i < 10; i++)
{
<p class="mb-4 leading-normal">
Bookings are confirmed once full payment has been received. Fares are quoted per
traveller in the currency shown at checkout and include all mandatory taxes and
carrier surcharges known at the time of booking. Changes to travel dates, routing
or traveller names are subject to the fare rules of the airline and may attract a
change fee. Refunds, where permitted, are processed to the original form of payment
within fourteen days of the request being approved.
</p>
}
</div>
<sa-dialog-footer>
<sa-button variant="ButtonVariant.Outline" commandfor="--dialog-sticky-footer" command="close">
Close
</sa-button>
</sa-dialog-footer>
</sa-dialog>JavaScript API
Since <sa-dialog> renders a native dialog element, you can control it directly via JavaScript using the showModal(), show(), and close().
<sa-group justify="GroupJustify.Center">
<sa-button variant="ButtonVariant.Outline" id="--dialog-open-via-js-show-modal-button">
Show Modal
</sa-button>
<sa-button variant="ButtonVariant.Outline" id="--dialog-open-via-js-show-button">
Show
</sa-button>
</sa-group>
<sa-dialog id="--dialog-open-via-js-dialog">
This is the dialog content...
<sa-dialog-footer>
<sa-button variant="ButtonVariant.Outline" id="--dialog-open-via-js-close-button">
Close via JS
</sa-button>
</sa-dialog-footer>
</sa-dialog>
<script>
(function() {
const dialog = document.getElementById("--dialog-open-via-js-dialog");
const showModalButton = document.getElementById("--dialog-open-via-js-show-modal-button");
const showButton = document.getElementById("--dialog-open-via-js-show-button");
const closeButton = document.getElementById("--dialog-open-via-js-close-button");
showModalButton.addEventListener("click", () => {
dialog.showModal();
});
showButton.addEventListener("click", () => {
dialog.show();
});
closeButton.addEventListener("click", () => {
dialog.close();
});
})();
</script>JavaScript events
The dialog fires beforetoggle and toggle events as it opens and closes, a cancel event when the user attempts to close via Esc or request-close, and a close event once closed. Call preventDefault() in beforetoggle to prevent opening, or in cancel to prevent closing.
<sa-stack gap="StackGap.Small" align="StackAlign.Start" class="min-w-md">
<sa-button variant="ButtonVariant.Outline" commandfor="--dialog-js-events-dialog" command="show-modal">
Open Dialog
</sa-button>
<sa-input type="checkbox"
id="--dialog-js-events-prevent-toggle-checkbox"
label="PreventDefault() in beforetoggle event"
description="Will prevent the dialog from opening"/>
<label class="text-sm font-bold">Events:</label>
<div class="font-mono w-full h-40 overflow-y-auto rounded-md border border-border bg-muted p-3 text-foreground" id="--dialog-js-events-output">
</div>
</sa-stack>
<sa-dialog id="--dialog-js-events-dialog">
<sa-dialog-header>
<sa-dialog-title>Events</sa-dialog-title>
</sa-dialog-header>
<p>This dialog is used to demonstrates events.</p>
<sa-input type="checkbox"
id="--dialog-js-events-prevent-cancel-checkbox"
label="PreventDefault() in cancel event"
description="Will prevent the Request Close button from closing the dialog"/>
<sa-dialog-footer>
<sa-button variant="ButtonVariant.Outline" commandfor="--dialog-js-events-dialog" command="close">
Close
</sa-button>
<sa-button variant="ButtonVariant.Outline" commandfor="--dialog-js-events-dialog" command="request-close">
Request Close
</sa-button>
</sa-dialog-footer>
</sa-dialog>
<script>
(function () {
let counter = 0;
const dialog = document.getElementById('--dialog-js-events-dialog');
const output = document.getElementById('--dialog-js-events-output');
const preventToggleCheckbox = document.getElementById('--dialog-js-events-prevent-toggle-checkbox');
const preventCancelCheckbox = document.getElementById('--dialog-js-events-prevent-cancel-checkbox');
dialog.addEventListener("beforetoggle", (e) => {
if (preventToggleCheckbox.checked === true) {
output.innerText = `${++counter}: beforetoggle event prevented!\n` + output.innerText;
e.preventDefault();
return;
}
output.innerText = `${++counter}: beforetoggle event: ${e.oldState} -> ${e.newState}\n` + output.innerText;
});
dialog.addEventListener("toggle", (e) => {
output.innerText = `${++counter}: toggle event: ${e.oldState} -> ${e.newState}\n` + output.innerText;
});
dialog.addEventListener("cancel", (e) => {
if (preventCancelCheckbox.checked === true) {
output.innerText = `${++counter}: cancel event prevented!\n` + output.innerText;
e.preventDefault();
return;
}
output.innerText = `${++counter}: cancel event\n` + output.innerText;
});
dialog.addEventListener("close", (e) => {
output.innerText = `${++counter}: close event\n` + output.innerText;
});
})();
</script>Accessibility
<sa-dialog> renders a native <dialog> element that is opened with showModal(), so the browser provides the modal behaviour. Focus is trapped inside the dialog, the rest of the page is inert, Escape closes it and focus returns to the trigger when it closes.
The Tag Helper does not link the title to the dialog. Give <sa-dialog-title> an id and reference it from aria-labelledby on <sa-dialog> so assistive technology announces the dialog by its title, and do the same with aria-describedby and <sa-dialog-description> when there is one. Add autofocus to the control that should receive focus when the dialog opens, and always include a visible way to close the dialog, since no close button is rendered for you.
API Reference
<sa-dialog>
Renders a native <dialog> element. The dialog is opened and closed via the Invoker Commands API: a button with command="show-modal" or command="close" and a commandfor attribute referencing the dialog's id.
The dialog is wrapped in a <sel-dialog> web component that locks page scrolling while it is open. See JavaScript & Interactivity.
<sa-dialog-header>
Renders a <div> element that groups the dialog's title and description.
<sa-dialog-title>
Renders an <h2> element.
<sa-dialog-description>
Renders a <p> element.
<sa-dialog-footer>
Renders a <div> element that hosts the dialog's action buttons.