Skip to content

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 the open attribute or the imperative Modal.confirm / Modal.info helpers.

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.

Open Modal

This is the modal content.

OK
html
<r-button onclick="modal.open = true">Open Modal</r-button>

<r-modal id="modal" title="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

PropertyTypeDefaultDescription
openbooleanfalseWhether the modal is visible
titlestring''Header title text (falls back to Modal when empty)
closablebooleantrueWhether the close (x) button is shown
maskClosablebooleantrueWhether clicking the backdrop mask closes the modal
closeOnEscbooleantrueWhether pressing Escape closes the modal
lockScrollbooleantrueWhether body scroll is locked while the modal is open
autoFocusbooleantrueWhether the first focusable element is focused on open
hideHeaderbooleanfalseDrops the title bar entirely, leaving a floating close button
sheetstring''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

html
<r-modal open title="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.

html
<r-modal open title="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.

html
<r-modal open title="Unsaved changes" maskClosable="false">
  <p>Clicking outside will not dismiss this dialog.</p>
</r-modal>

Close on Escape closeOnEsc

html
<r-modal open title="Report" closeOnEsc="false">
  <p>The Escape key is disabled for this dialog.</p>
</r-modal>

Lock Scroll lockScroll

html
<r-modal open title="Preview" lockScroll="false">
  <p>The page behind the modal can still scroll.</p>
</r-modal>

Auto Focus autoFocus

html
<r-modal open title="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.

html
<r-modal open hide-header>
  <img src="/diagram.png" alt="Architecture diagram" style="display: block; max-width: 100%;" />
</r-modal>

Slots

SlotDescription
(default)Body content of the modal
footerFooter actions; the footer bar only shows when this is filled
html
<r-modal open title="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'.

EventCancelabledetailDescription
beforeopenYesBefore opening; call preventDefault() to cancel
openNoFired when the modal opens
afteropenNoFired after the open transition finishes
beforecloseYes{ trigger }Before closing; call preventDefault() to cancel
closeNo{ trigger }Fired when the modal closes
aftercloseNo{ trigger }Fired after the close transition finishes
html
<r-modal id="modal" title="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'.

MethodDescription
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.

js
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().

PartDescription
rootOuter overlay container
maskBackdrop behind the dialog
dialogThe dialog box
headerHeader bar
titleTitle heading
closeClose (x) button
bodyScrollable body region
footerFooter action bar
css
r-modal::part(dialog) {
  border-radius: 8px;
}
r-modal::part(mask) {
  background: rgba(0, 0, 0, 0.6);
}

Best Practices

  • Trigger + toggle: Open with modal.open = true and close with modal.open = false, or call close().
  • Guard destructive closes: Listen for beforeclose and preventDefault() 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" and maskClosable="false" to force an explicit choice.
  • One-off dialogs: Use Modal.confirm / Modal.info for 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]open is removed the instant close() runs, but the mask/dialog transition keeps painting for ~0.3s more, and dropping a z-index escalation mid-fade lets the still-visible mask repaint under whatever it was lifted above.

Released under the MIT License.