Modal
<r-modal>APIStyle tokensPlaygroundSource
Dialog component for focused interactions on top of the current page, with focus trapping, scroll locking, and background inerting.
Use when you need a dialog for a focused interaction over the page, with focus trapping, scroll locking, and background inerting. Drive
<r-modal>via theopenattribute or the imperativeModal.confirm/Modal.infohelpers.
Listen for beforeclose and call preventDefault() to confirm before discarding unsaved work. Put actions in slot="footer"; the footer only appears when that slot has content. For non-dismissible flows, set closable="false" and maskClosable="false". While open, escalate stacking with :has(r-modal[open]), :has(r-modal[closing]) so the mask stays above during the close transition.
Quick Start
Basic Usage
Modal visibility is controlled by the open attribute (or the open property). It starts closed and renders nothing until opened, so wire a trigger to toggle it.
This is the modal content.
<r-button onclick="modal.open = true">Open Modal</r-button>
<r-modal id="modal" heading="Basic Modal">
<p>This is the modal content.</p>
<div slot="footer">
<button tabindex="0" type="button" onclick="modal.open = false">OK</button>
</div>
</r-modal>API Reference
Properties
| Property | Type | Default | Description |
|---|---|---|---|
open |
boolean |
false |
Whether the modal is visible |
heading |
string |
'' |
Header title text (falls back to Modal when empty) |
closable |
boolean |
true |
Whether the close button is shown |
maskClosable |
boolean |
true |
Whether clicking the backdrop mask closes the modal |
closeOnEsc |
boolean |
true |
Whether pressing Escape closes the modal |
lockScroll |
boolean |
true |
Whether body scroll is locked while the modal is open |
autoFocus |
boolean |
true |
Whether the first focusable element is focused on open |
hideHeader |
boolean |
false |
Drops the title bar entirely, leaving a floating close button |
sheet |
string |
'' |
CSS injected into the shadow DOM |
closing is a read-only attribute the element reflects on itself (not a settable property): present from the moment close() runs until the mask/dialog's fade-and-scale-out transition actually finishes (~0.3s later, same timing as the afterclose event). Useful for a host page that needs the modal to still count as "present" through that visual tail.
Title title
<r-modal open heading="Delete item">
<p>Are you sure you want to delete this item?</p>
</r-modal>Closable closable
Hides the header close button so the modal can only be dismissed through your own controls.
<r-modal open heading="Terms" closable="false">
<p>You must accept the terms to continue.</p>
<div slot="footer">
<button tabindex="0" type="button">Accept</button>
</div>
</r-modal>Mask Closable maskClosable
By default clicking the backdrop closes the modal. Set to false to require an explicit action.
<r-modal open heading="Unsaved changes" maskClosable="false">
<p>Clicking outside will not dismiss this dialog.</p>
</r-modal>Close on Escape closeOnEsc
<r-modal open heading="Report" closeOnEsc="false">
<p>The Escape key is disabled for this dialog.</p>
</r-modal>Lock Scroll lockScroll
<r-modal open heading="Preview" lockScroll="false">
<p>The page behind the modal can still scroll.</p>
</r-modal>Auto Focus autoFocus
<r-modal open heading="Search" autoFocus="false">
<input type="text" placeholder="Type to search" />
</r-modal>Headerless Mode hideHeader
Drops the title bar and its border entirely, leaving only a floating close button (top-right) when closable. Suited to content-only dialogs like image or diagram lightboxes, where a title bar would just eat into the content. The dialog keeps an accessible name via aria-label (derived from title) even though the visible <h3> title is gone, so set title for a screen-reader label even in headerless mode.
<r-modal open hide-header>
<img src="/diagram.png" alt="Architecture diagram" style="display: block; max-width: 100%;" />
</r-modal>Slots
| Slot | Description |
|---|---|
| (default) | Body content of the modal |
footer |
Footer actions; the footer bar only shows when this is filled |
<r-modal open heading="Confirm">
<p>Body content goes in the default slot.</p>
<div slot="footer">
<button tabindex="0" type="button" onclick="modal.open = false">Cancel</button>
<button tabindex="0" type="button">Confirm</button>
</div>
</r-modal>Events
All close-related events carry a trigger in event.detail describing what caused the close: 'mask', 'button', 'escape', or 'program'.
| Event | Cancelable | detail |
Description |
|---|---|---|---|
beforeopen |
Yes | — | Before opening; call preventDefault() to cancel |
open |
No | — | Fired when the modal opens |
afteropen |
No | — | Fired after the open transition finishes |
beforeclose |
Yes | { trigger } |
Before closing; call preventDefault() to cancel |
close |
No | { trigger } |
Fired when the modal closes |
afterclose |
No | { trigger } |
Fired after the close transition finishes |
<r-modal id="modal" heading="Example"></r-modal>
<script>
const modal = document.getElementById('modal');
modal.addEventListener('beforeclose', (e) => {
if (!confirm('Discard changes?')) e.preventDefault();
});
modal.addEventListener('close', (e) => {
console.log('closed via', e.detail.trigger); // 'mask' | 'button' | 'escape' | 'program'
});
</script>Programmatic API
The Modal class exposes static helpers that create, mount, and resolve a modal without markup. Each returns a Promise<{ action, trigger }> where action is 'confirm', 'cancel', or 'dismiss'.
| Method | Description |
|---|---|
Modal.open(opts) |
Open a modal with a single OK button |
Modal.confirm(opts) |
Open a modal with OK and Cancel buttons |
Modal.info(opts) |
Informational modal (title defaults to Info) |
Modal.success(opts) |
Success modal (title defaults to Success) |
Modal.warning(opts) |
Warning modal (title defaults to Warning) |
Modal.error(opts) |
Error modal (title defaults to Error) |
Options (all optional): title, content, okText, cancelText, showCancel, maskClosable, closeOnEsc, lockScroll, autoFocus, closable, onConfirm, onCancel. onConfirm / onCancel may return false (or a promise resolving to false) to keep the modal open.
import { Modal } from 'ranui/modal';
const result = await Modal.confirm({
title: 'Delete project',
content: 'This action cannot be undone.',
okText: 'Delete',
cancelText: 'Keep',
onConfirm: async () => {
await deleteProject();
},
});
if (result.action === 'confirm') {
// deleted
}CSS Parts
Style internal pieces with ::part().
| Part | Description |
|---|---|
root |
Outer overlay container |
mask |
Backdrop behind the dialog |
dialog |
The dialog box |
header |
Header bar |
title |
Title heading |
close |
Close button |
body |
Scrollable body region |
footer |
Footer action bar |
r-modal::part(dialog) {
border-radius: 8px;
}
r-modal::part(mask) {
background: rgba(0, 0, 0, 0.6);
}Styling
<r-modal> exposes 23 CSS custom properties of its own, plus the semantic tokens it reads
from the theme. Set one anywhere it inherits from, such as :root, a wrapper, or the element:
r-modal {
--ran-modal-mask-background: var(--ran-color-bg-subtle);
}Parts: body · close · dialog · footer · header · mask · root · title
The full list is in style tokens; which token to use is in the design system.
Playground
This is the modal content.