Composite
Dialog Requires JS
A modal overlay built on the native <dialog> element. Focus trap, keyboard handling, click-outside dismiss, and smooth entry / exit animations come for free. Available as a CSS class set or as the <ren-dialog> Web Component with auto-wired triggers.
About
Overview
A dialog interrupts the user's current flow to confirm an action, gather a small piece of input, or surface a focused decision. RenDS uses the browser's native <dialog> element under the hood, so a lot of the hard parts — modal scroll lock, top-layer rendering, focus trapping, escape-to-close — come from the platform. The component layers smooth animations, mobile sheet behavior, and declarative auto-wiring on top.
Use a dialog when the user needs to make a deliberate choice before the rest of the UI continues. If the action is destructive or irreversible, use the alert variant so the dialog can't be dismissed by clicking the backdrop.
Don't reach for a dialog by default. If the decision can live on the page (an inline form, a popover, an expandable panel), prefer those. A modal blocks everything else and shouldn't be the answer to "where do I put this content?".
Parts
Anatomy
A dialog is composed of five regions. Three are required (container, body, footer with at least one action), two are conventional (header with title and close button, backdrop).
Assembled
The dialog box itself — <dialog class="ren-dialog">. Sized by a width variant token (--dialog-max-width). Renders in the browser's top layer.
The dimmed scrim. Styled via ::backdrop. Click to dismiss (unless alert is set). Fades in with the dialog.
.ren-dialog-header with .ren-dialog-title and an optional close button (data-dialog-close). Bottom-bordered, padded.
.ren-dialog-body. Scrolls when content exceeds the dialog's max-height. Independent scrollbar so the page beneath stays locked.
.ren-dialog-footer with action buttons. Conventionally right-aligned, primary action last. Top-bordered.
Live
Demo
Click any trigger to open the dialog. Esc closes it; clicking outside the dialog closes it (except for alert dialogs). Tab cycles through the focusable elements without escaping.
<button data-dialog-trigger="dlg-confirm">Open dialog</button>
<ren-dialog id="dlg-confirm">
<dialog class="ren-dialog">
<div class="ren-dialog-header">
<h2 class="ren-dialog-title">Confirm changes</h2>
<button class="ren-dialog-close" data-dialog-close aria-label="Close">×</button>
</div>
<div class="ren-dialog-body">
Save the edits to your profile?
</div>
<div class="ren-dialog-footer">
<button class="ren-btn ren-btn-secondary" data-dialog-close>Cancel</button>
<button class="ren-btn ren-btn-primary">Save</button>
</div>
</dialog>
</ren-dialog>
Shapes
Variants
Five sizes plus an alert mode. Default is md. Pick the smallest size that fits the content — bigger isn't friendlier.
Reference
API
CSS classes
Apply directly to the <dialog> element. They work even if you don't use the Web Component (you'd just script open / close yourself).
| Class | Effect |
|---|---|
.ren-dialog |
Base. Required on the <dialog> element. Provides background, border-radius, shadow, animations, and the backdrop styling. |
.ren-dialog-header |
Top region. Flex row with a bottom border. Holds the title and close button. |
.ren-dialog-title |
Heading inside the header. Set on the actual heading tag (<h2>, <h3>) so screen readers announce it correctly. |
.ren-dialog-description |
Optional sub-title under the title. Muted color. |
.ren-dialog-body |
Main content region. Scrolls independently when its content exceeds max-height: 85dvh. |
.ren-dialog-footer |
Bottom region for action buttons. Top-bordered, right-aligned by default. |
.ren-dialog-close |
The icon close button placed in the header. Square, ghost-styled, focus ring matches the system token. |
.ren-dialog-sm / -md / -lg / -xl / -full |
Size modifiers. Set --dialog-max-width: 24rem · 32rem · 42rem · 56rem · 95dvw. |
.ren-alert-dialog |
Alert flavor. Suppresses click-outside dismissal. Used by <ren-alert-dialog> internally. |
Web Component attributes
Set on the <ren-dialog> wrapper. All are reactive — flipping them in JS opens, closes, or resizes the dialog without rebuilding the markup.
| Attribute | Type | Default | Notes |
|---|---|---|---|
open |
boolean | false | Opens the dialog when present. Mirrors <dialog open> but routes through the component lifecycle (events, focus trap). |
size |
"sm" | "md" | "lg" | "xl" | "full" | "md" | Width variant. Adds the matching .ren-dialog-* class to the inner dialog. |
alert |
boolean | false | Disables backdrop click dismissal. The user must press a button to close. |
no-escape |
boolean | false | Suppresses Esc closing the dialog. Use sparingly — it traps the user. |
JavaScript methods
Available on every <ren-dialog> instance. All return Promises so you can await open / close transitions before continuing.
| Method | Description |
|---|---|
show() / open() |
Opens the dialog. Returns a Promise that resolves once the entry animation finishes. Aliases. |
close(returnValue?) |
Closes the dialog. Optional returnValue is forwarded to the ren-close event detail. Promise resolves after the exit animation. |
isOpen |
Boolean getter. true while the dialog is visible. |
dialog |
Getter that returns the underlying native <dialog> element if you need to call its DOM API directly. |
const dialog = document.querySelector('ren-dialog');
await dialog.show(); // wait for it to be visible
const result = await dialog.close('confirmed'); // returnValue surfaces in ren-close
if (dialog.isOpen) { /* still open */ }Events
Both events bubble and are composed. Listen on the wrapper or any ancestor.
| Event | Detail |
|---|---|
ren-open |
Fired after the entry animation completes. event.detail is empty. |
ren-close |
Fired after the exit animation completes. event.detail.returnValue contains the value passed to close(), if any. |
Auto-wiring
You don't need to write JavaScript for the common case. Two declarative attributes wire opening and closing:
| Attribute | Effect |
|---|---|
data-dialog-trigger="ID" |
On any element. Click opens the <ren-dialog> with the matching id. |
data-dialog-close |
On any element inside the dialog. Click closes the parent dialog. |
Inclusive by default
Accessibility
Because the component renders an actual <dialog> element, the platform handles most of the work. RenDS adds the focus management, motion preferences, and labelling conventions on top.
Keyboard
no-escape is set.
Focus management
- On open, focus moves to the first focusable element inside the dialog. To override, mark a specific element with
autofocus. - While open, focus is trapped inside the dialog — Tab and Shift+Tab cycle without leaving.
- On close, focus returns to the element that opened the dialog (the trigger button). The user never has to find their place again.
Labelling
Always pair the dialog with an accessible name. The simplest path is to give the title an id and reference it via aria-labelledby:
<ren-dialog id="dlg-labelling" aria-labelledby="dlg-labelling-title">
<dialog class="ren-dialog">
<div class="ren-dialog-header">
<h2 class="ren-dialog-title" id="dlg-labelling-title">Delete file</h2>
</div>
...
</dialog>
</ren-dialog>If you also have descriptive prose under the title, link it with aria-describedby.
Motion
Entry and exit animations use the --duration-enter motion token. Under prefers-reduced-motion: reduce the transitions are disabled — the dialog appears and disappears instantly.
Use the alert variant for destructive confirmations. A backdrop click on a regular dialog dismisses it silently — fine for most cases, dangerous when the user is about to delete something. The alert variant forces an explicit button choice.
Patterns
Examples
Confirmation
The classic: a dialog asks the user to confirm a non-trivial action.
<button data-dialog-trigger="dlg-save">Save</button>
<ren-dialog id="dlg-save" aria-labelledby="save-title">
<dialog class="ren-dialog">
<div class="ren-dialog-header">
<h2 class="ren-dialog-title" id="save-title">Save changes?</h2>
</div>
<div class="ren-dialog-body">
Your edits will be visible to the team immediately.
</div>
<div class="ren-dialog-footer">
<button class="ren-btn ren-btn-secondary" data-dialog-close>Cancel</button>
<button class="ren-btn ren-btn-primary">Save</button>
</div>
</dialog>
</ren-dialog>Destructive (alert)
Backdrop click is suppressed; the user must press one of the two buttons.
<button class="ren-btn ren-btn-danger" data-dialog-trigger="dlg-delete">Delete account</button>
<ren-alert-dialog id="dlg-delete" aria-labelledby="delete-title">
<dialog class="ren-dialog ren-alert-dialog">
<div class="ren-dialog-header">
<h2 class="ren-dialog-title" id="delete-title">Delete this account?</h2>
</div>
<div class="ren-dialog-body">
All projects, files, and history will be permanently removed. This cannot be undone.
</div>
<div class="ren-dialog-footer">
<button class="ren-btn ren-btn-secondary" data-dialog-close>Keep account</button>
<button class="ren-btn ren-btn-danger">Delete account</button>
</div>
</dialog>
</ren-alert-dialog>Form dialog
Using a dialog to collect a small piece of input. Submit the form to close.
<button data-dialog-trigger="dlg-rename">Rename project</button>
<ren-dialog id="dlg-rename" aria-labelledby="dlg-rename-title">
<dialog class="ren-dialog ren-dialog-sm">
<form method="dialog">
<div class="ren-dialog-header">
<h2 class="ren-dialog-title" id="dlg-rename-title">Rename project</h2>
</div>
<div class="ren-dialog-body">
<label for="project-name" class="ren-field-label">New name</label>
<input id="project-name" name="project-name" class="ren-input" autofocus required>
</div>
<div class="ren-dialog-footer">
<button class="ren-btn ren-btn-secondary" type="button" data-dialog-close>Cancel</button>
<button class="ren-btn ren-btn-primary" type="submit">Rename</button>
</div>
</form>
</dialog>
</ren-dialog>Reading the result
When you need to know which button closed the dialog, listen to ren-close and pass a returnValue.
const dialog = document.querySelector('#dlg-save');
dialog.querySelector('.ren-btn-primary')
.addEventListener('click', () => dialog.close('save'));
dialog.querySelector('.ren-btn-secondary')
.addEventListener('click', () => dialog.close('cancel'));
dialog.addEventListener('ren-close', (e) => {
if (e.detail.returnValue === 'save') {
persist();
}
});