Modal dialog on the scrim, in three widths, with a heading, a body and a sunken footer for actions. The host sets open; Escape, the scrim and the corner X each ask to close and can be refused. Reach for it when the user must finish or abandon one task before returning to the page.
Usage
<sui-dialog heading="Record payment" subtitle="Applies to open invoices for this customer">
<div style="font-size:13.5px">Customer: Silver Hills Church<br>Amount: $6,420.00<br>Deposit to: Operating 4402</div>
<span slot="footer"><sui-button variant="ghost" label="Cancel" data-close></sui-button><sui-button variant="brand" label="Save payment" data-close></sui-button></span>
</sui-dialog>
<script type="module">
import { defineCustomElements } from "@shineui/core/loader";
import "@shineui/core/shineui.css";
defineCustomElements();
const el = document.querySelector("sui-dialog");
el.addEventListener("suiClose", (e) => console.log(e.detail));
</script>
import { SuiDialog } from "@shineui/react";
import "@shineui/core/shineui.css";
export function Example() {
return (
<SuiDialog heading="Record payment" subtitle="Applies to open invoices for this customer" onSuiClose={(e) => console.log(e.detail)}>
<div style="font-size:13.5px">Customer: Silver Hills Church<br>Amount: $6,420.00<br>Deposit to: Operating 4402</div>
<span slot="footer"><sui-button variant="ghost" label="Cancel" data-close></sui-button><sui-button variant="brand" label="Save payment" data-close></sui-button></span>
</SuiDialog>
);
}
<script setup>
import { SuiDialog } from "@shineui/vue";
import "@shineui/core/shineui.css";
const onChange = (e) => console.log(e.detail);
</script>
<template>
<SuiDialog heading="Record payment" subtitle="Applies to open invoices for this customer" @sui-close="onChange"><div style="font-size:13.5px">Customer: Silver Hills Church<br>Amount: $6,420.00<br>Deposit to: Operating 4402</div>
<span slot="footer"><sui-button variant="ghost" label="Cancel" data-close></sui-button><sui-button variant="brand" label="Save payment" data-close></sui-button></span></SuiDialog>
</template>
import { Component } from "@angular/core";
import { SuiDialog } from "@shineui/angular";
@Component({
standalone: true,
imports: [SuiDialog],
template: `<sui-dialog heading="Record payment" subtitle="Applies to open invoices for this customer" [(open)]="open" (suiClose)="onChange($event)"><div style="font-size:13.5px">Customer: Silver Hills Church<br>Amount: $6,420.00<br>Deposit to: Operating 4402</div>
<span slot="footer"><sui-button variant="ghost" label="Cancel" data-close></sui-button><sui-button variant="brand" label="Save payment" data-close></sui-button></span></sui-dialog>`,
})
export class Example {
open = false;
onChange(e: CustomEvent) { console.log(e.detail); }
}
Basic
The card is role="dialog", named from heading, and the page behind it is inert. Focus opens on the first field in the body, on the body itself when it scrolls, or on the card, and returns to the opener on close.
Customer: Silver Hills Church Amount: $6,420.00 Deposit to: Operating 4402
<sui-button id="open-md" variant="accent" label="Record payment"></sui-button>
<sui-dialog id="dlg-md" heading="Record payment" subtitle="Applies to open invoices for this customer">
<div style="font-size:13.5px">Customer: Silver Hills Church<br>Amount: $6,420.00<br>Deposit to: Operating 4402</div>
<span slot="footer"><sui-button variant="ghost" label="Cancel" data-close></sui-button><sui-button variant="brand" label="Save payment" data-close></sui-button></span>
</sui-dialog>
sm is 400px, md 520px and lg 680px. hide-close removes the corner X only: Escape and the scrim still ask to close through suiClose, and the host decides.
This posts a reversing entry dated today.A wide dialog for previews and record editors.Escape and the scrim still ask to close; the host decides.
<div style="display:flex;gap:10px;flex-wrap:wrap">
<sui-button id="open-sm" variant="outline" label="Small"></sui-button>
<sui-button id="open-lg" variant="outline" label="Large"></sui-button>
<sui-button id="open-lock" variant="outline" label="No close control"></sui-button>
</div>
<sui-dialog id="dlg-sm" heading="Void invoice INV-1042?" size="sm">This posts a reversing entry dated today.<span slot="footer"><sui-button variant="ghost" label="Keep" data-close></sui-button><sui-button variant="danger" label="Void invoice" data-close></sui-button></span></sui-dialog>
<sui-dialog id="dlg-lg" heading="Invoice preview" subtitle="INV-1043" size="lg">A wide dialog for previews and record editors.<span slot="footer"><sui-button variant="brand" label="Done" data-close></sui-button></span></sui-dialog>
<sui-dialog id="dlg-lock" heading="Closing July" subtitle="Do not leave this page" hide-close>Escape and the scrim still ask to close; the host decides.<span slot="footer"><sui-button variant="brand" label="Finish" data-close></sui-button></span></sui-dialog>
The card is capped at 76% of the viewport height. The body scrolls on its own while the header and the footer stay put; focus opens on it, so the arrow and page keys scroll the terms rather than the page behind, and the wheel stays on the card when it reaches either end.
1. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
2. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
3. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
4. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
5. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
6. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
7. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
8. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
9. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
10. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
<sui-button id="open-long" variant="outline" label="Read the terms"></sui-button>
<sui-dialog id="dlg-long" heading="Terms of engagement" subtitle="Effective Aug 1, 2026">
<p style="margin:0 0 12px">1. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<p style="margin:0 0 12px">2. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<p style="margin:0 0 12px">3. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<p style="margin:0 0 12px">4. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<p style="margin:0 0 12px">5. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<p style="margin:0 0 12px">6. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<p style="margin:0 0 12px">7. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<p style="margin:0 0 12px">8. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<p style="margin:0 0 12px">9. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<p style="margin:0 0 12px">10. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<span slot="footer"><sui-button variant="ghost" label="Decline" data-close></sui-button><sui-button variant="brand" label="Accept" data-close></sui-button></span>
</sui-dialog>
initial-focus is a selector resolved inside the dialog for the control that opens with focus; on a sui-button it lands on the inner button. Here it points at Keep, so Enter cannot void by accident, as sui-confirm-dialog does on its own. A miss falls back to the first field, the scrolling body or the card.
movable lets the pointer drag the card by its header; it stays inside the viewport and returns to the center on close. Keyboard users see no change, since the header takes no focus.
Silver Hills Church Issued Aug 8, 2026 Balance $6,420.00
<sui-button id="open-move" variant="outline" label="Compare with the list"></sui-button>
<sui-dialog id="dlg-move" movable heading="INV-1042" subtitle="Drag the header to see the page behind">
<div style="font-size:13.5px">Silver Hills Church<br>Issued Aug 8, 2026<br>Balance $6,420.00</div>
<span slot="footer"><sui-button variant="brand" label="Done" data-close></sui-button></span>
</sui-dialog>
maximizable adds a control beside the X that fills the viewport and restores; the state survives a close. maximized is a prop, so a host can open the dialog already maximized, and suiMaximizedChange reports each toggle.
Not maximized.
1. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
2. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
3. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
4. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
5. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
6. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
7. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
8. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
9. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
10. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
<sui-button id="open-max" variant="outline" label="Invoice preview"></sui-button>
<sui-button id="open-max-now" variant="outline" label="Open maximized"></sui-button>
<span id="dlg-max-log" style="font-size:12px;color:var(--ink-muted)">Not maximized.</span>
</div>
<sui-dialog id="dlg-max" maximizable heading="Invoice preview" subtitle="INV-1043" size="lg">
<p style="margin:0 0 12px">1. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<p style="margin:0 0 12px">2. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<p style="margin:0 0 12px">3. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<p style="margin:0 0 12px">4. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<p style="margin:0 0 12px">5. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<p style="margin:0 0 12px">6. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<p style="margin:0 0 12px">7. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<p style="margin:0 0 12px">8. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<p style="margin:0 0 12px">9. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<p style="margin:0 0 12px">10. Invoices are due 30 days from the issue date. A late fee of 1.5% per month applies to any balance still open after the grace period, and disputed lines must be raised in writing before the due date.</p>
<span slot="footer"><sui-button variant="ghost" label="Close" data-close></sui-button><sui-button variant="brand" label="Send" data-close></sui-button></span>
</sui-dialog>
The header slot replaces heading and subtitle and supplies the accessible name, so the title can carry a chip. The body here has a field, so focus opens on it.
A select in the body opens its list over the card. When the dialog closes with that list still open, as it does here when the save finishes, the list closes with it and its own suiClose says outside; with focus in the list, Escape closes the list first and the dialog stays. Click Save, then open the owner list.
The dialog stays where it is in the page and paints in the top layer while open, so a transformed or clipping box around it cannot trap it, and a form around it owns its fields as it owns any: Save submits the amount with the customer, and the form's own input and change listeners hear them.
Nothing posted yet.
<form id="dlg-form-demo" style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap">
<sui-input-text name="customer" label="Customer" value="Silver Hills Church"></sui-input-text>
<sui-button id="open-form" variant="outline" label="Record a payment"></sui-button>
<sui-dialog id="dlg-form" heading="Record payment" subtitle="Posts with the customer field on the page">
<sui-input-number name="amount" label="Amount" value="6420"></sui-input-number>
<span slot="footer"><sui-button variant="ghost" label="Cancel" data-close></sui-button><sui-button variant="brand" type="submit" label="Save payment"></sui-button></span>
</sui-dialog>
</form>
<div id="dlg-form-log" style="margin-top:10px;font-size:12px;color:var(--ink-muted)">Nothing posted yet.</div>
suiClose fires before the dialog closes and says why: escape, scrim or close-button. Here a stray scrim click is refused with preventDefault while the note is unsaved; Escape and the X still close.
// setup(root): properties and events
setup(root) {
const dlg = wire(root, 'open-dirty', 'dlg-dirty');
const log = root.querySelector('#dlg-log');
dlg.addEventListener('suiClose', (e) => {
if (e.detail === 'scrim') {
e.preventDefault();
log.textContent = 'Refused a scrim dismissal. The note is unsaved.';
return;
}
log.textContent = 'Closed by ' + e.detail + '.';
});
}
Keyboard
Key
Action
TabShift+Tab
Cycles through the controls in the card and never leaves it. A panel opened from the card, a select list or a popover, counts as inside; focus that lands behind the scrim by any other route is brought back.
Escape
Asks to close with reason escape, maximized or not. Only the topmost open surface answers it: a popover, confirm popup or select list open inside the dialog takes it first and the dialog stays.
EnterSpace
On the corner X, asks to close with reason close-button. On the maximize control, fills the viewport or restores the card.
ArrowDownArrowUpPageDownPageUp
Scroll the body when it overflows and holds focus, which is where a prose dialog opens.
API
<sui-dialog>
Properties
Property
Attribute
Type
Default
Description
heading
heading
string
''
Dialog title (the reference title prop; renamed for the native attribute). Supplies the accessible name unless a header slot is given.
hideClose
hide-close
boolean
false
Removes the corner close control, for a dialog the reader must finish through its own actions. It removes the button only: Escape and the scrim still ask to close through suiClose, and the host decides, so refuse those reasons there when the dialog must not go. A modal offers a way out by default, which is why the X shows unless this is set.
initialFocus
initial-focus
stringstring | undefined
A selector for the control that takes focus when the dialog opens, resolved inside the dialog: #save, or .keep for a sui-button, whose inner button then takes it. Without it focus lands on the first field in the body, else on the body itself when it scrolls, so the arrow and page keys scroll the dialog and not the page behind it, else on the card; never on the corner controls. sui-confirm-dialog points it at Cancel, the least destructive action. A selector that matches nothing, or a disabled control, falls through to the same order.
maximizable
maximizable
boolean
false
Adds a control beside the corner X that toggles maximized.
maximized
maximized
boolean
false
Fills the viewport, with no margin and no rounded corners. The host can set it to open the dialog already maximized; the maximize control toggles it and emits suiMaximizedChange. It is not cleared on close, so a reopened dialog comes back the way it was left.
movable
movable
boolean
false
Lets the pointer move the card by its header. The card stays inside the viewport and returns to the center on close. Keyboard users see no change: the card is not moved from the keyboard, and Tab never lands on the header. (Named movable because draggable is a native attribute that would switch on browser drag-and-drop for the host.)
open
open
boolean
false
Shows or hides the dialog. The host sets it; on a close request the element clears it itself unless the suiClose listener calls preventDefault(), so plain HTML works. When the dialog closes, a popup still open inside it, a select's list or a popover, closes with it and asks through its own suiClose with reason outside; a popup that has focus takes Escape first and the dialog stays. The element never opens itself, so there is no suiOpen; watch open. While open, and while it fades out, the dialog paints in the browser's top layer, above the rest of the page, so a transformed, clipping or stacked ancestor cannot trap or bury it. It stays in place in the element, so a listener on an ancestor, a <form> around the element and inherited styles reach the slotted content as they would anywhere else. Everything outside the dialog is inert and the page does not scroll until it closes; a node the page adds outside it while it is open goes inert too. The card is role="dialog" with no aria-modal: the inert outside is what makes it modal to assistive technology, and with aria-modal VoiceOver would drop the toast region outside it. Toasts keep announcing, and Tab goes on from the dialog's last control into the toast stack and back. An element the page marks data-sui-keep-live (another library's dropdown appended to the body, a chat widget) stays usable with everything in it: it never goes inert, focus may move into it and back, and Tab reaches it the same way. The dialog paints in the top layer, so a marked panel that should show above it must paint there too, as a popover does. On a viewport too short for the card with its header and footer pinned (a phone on its side, a page zoomed to 400%), the whole card scrolls instead of only its body, so the actions stay in reach.
returnFocus
return-focus
stringstring | undefined
A selector for the control that takes focus when the dialog closes, resolved in the document (or the shadow root) the element sits in when it closes, so it can name a row the dialog's own action created: #inv-1043, or .new-invoice for a sui-button, whose inner button then takes it. Without it focus returns to the control that had it when the dialog opened; when that control has left the page (a Delete button whose row the dialog removed) or can no longer take focus (a row in a menu that has closed), it goes to the nearest control in its place: the next one after it, else the one before it, so a menu's row hands it to the menu's trigger. A selector that matches nothing, or a disabled control, falls through to the same order.
Fires before the dialog closes itself; detail is 'escape' | 'scrim' | 'close-button'. Cancelable: preventDefault() keeps it open. Refusing one reason, such as scrim while a form is dirty, leaves the others working; there is no prop for it. It does not bubble, so a listener on the dialog never hears a select or a popover closing inside it.
suiMaximizedChange
boolean
onSuiMaximizedChange
@sui-maximized-change
Fires after the maximize control toggles maximized; detail is the new state. A host write to the prop does not echo it. Does not bubble.
Slots
Slot
Description
(default)
Dialog body.
footer
Action buttons, right-aligned on the sunken footer.
header
Replaces heading / subtitle when the title needs a tag, an icon or a document number chip. It supplies the accessible name.
Types
SuiCloseReasontypeWhy a dismissible surface is closing. Detail of the cancelable suiClose. A consumer expresses "a stray backdrop click must not close this" by calling preventDefault() on the reason it does not accept, which is why none of these needs a prop of its own.
No members.
Theming
Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.