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
<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 = 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; dropping a z-index escalation mid-fade lets the still-visible mask repaint under whatever it was lifted above.