Modal
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.
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">
<r-button type="primary" onclick="modal.open = false">OK</r-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 (x) 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; see Best Practices below.
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">
<r-button type="primary">Accept</r-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">
<r-button onclick="modal.open = false">Cancel</r-button>
<r-button type="primary">Confirm</r-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 (x) 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.
Best Practices
- Trigger + toggle: Open with
modal.open = trueand close withmodal.open = false, or callclose(). - Guard destructive closes: Listen for
beforecloseandpreventDefault()to confirm before dismissing unsaved work. - Footer actions: Put primary/secondary buttons in
slot="footer"; the footer bar only appears when the slot has content. - Non-dismissible flows: Set
closable="false"andmaskClosable="false"to force an explicit choice. - One-off dialogs: Use
Modal.confirm/Modal.infofor quick prompts instead of authoring markup. - Escalating a host page above the modal while it's open: match
:has(r-modal[open]), :has(r-modal[closing]), not just[open].openis removed the instantclose()runs, but the mask/dialog transition keeps painting for ~0.3s more; dropping a z-index escalation mid-fade lets the still-visible mask repaint under whatever it was lifted above.