Rime

Modal

A frosted dialog with a focus trap, scroll lock, inert page, scroll-aware edges and a bottom sheet on phones.

Modal portals a dialog over a deep scrim: focus moves in (to initialFocus, the first field, or the first button), Tab is trapped, Esc and a scrim click close it unless dismissible is false or busy is set, the page behind goes inert, and focus returns to whatever opened it.

Everything in the header sits on one left edge: the tone's icon tile and the close button on one row, then the title and description full width beneath; nothing is stacked over the title. tone tints the header wash and the tile (danger, success, info); progress draws a segmented step rule for multi-step flows.

The body scrolls on its own. Its scroll edges are the portable pattern from components/ui/Scroll: a hairline and soft shade under the header once content has scrolled (data-scrolled) and over the footer while there is more below (data-more), written straight to the DOM by useScrollEdges with a passive listener, plus thin tinted scrollbars and contained overscroll. Under 640px every size becomes a bottom sheet (or the full screen with mobile="fullscreen").

Widths are tokens: --modal-width-sm 480px, -md 640px, -lg 880px. meta puts a quiet line under the title (a reference, "Step 1 of 2"); nothing is stacked over the title. inline renders the open panel in place, with no portal, scrim, focus trap or scroll lock, for docs and design reviews.

Installation

npx shadcn@latest add KezLahd/rime/modal

Also installs: base, button, icon, icon-button, scroll-area.

Usage

import { Button, Modal } from "@/components/ui";

<Modal
  open={open}
  onClose={() => setOpen(false)}
  title="Contact support"
  description="Ask a question about invoice INV-1042."
  footer={
    <>
      <Button variant="secondary" onClick={() => setOpen(false)}>Cancel</Button>
      <Button onClick={send}>Send question</Button>
    </>
  }
>
  …form fields…
</Modal>

Examples

Open

inline renders the panel in place: no portal, scrim, focus trap or scroll lock. For docs and reviews.

Form in a modal

Long content: scroll edges

Scroll the body: the header shade appears once content passes under it, and the footer shade shows while there is more.

Terms with a scroll-gated accept

Accept stays disabled until the body has been scrolled to the end. onScrollEnd comes from useScrollEdges, the same listener that clears data-more when nothing is left below. Open it and scroll.

ConfirmModal, danger, two steps

The irreversible button only appears on the review step, so a stray Enter cannot confirm.

API

Generated from the component's TypeScript types and JSDoc (scripts/gen-ui-registry.mjs), so it always matches the source.

Modal

Modal props
PropTypeDefaultDescription
openRequiredboolean
onCloseRequired() => voidCalled on Esc, scrim click and the close button, unless dismissible is false.
titleRequiredReactNode
metaReactNodeA quiet line under the title: a reference, "Step 1 of 2". Never stacked over the title.
descriptionReactNode
childrenReactNode
inlinebooleanRender the open panel in place, in normal flow: no portal, no scrim, no inert page, no scroll lock, no focus trap and no exit animation. For docs, thumbnails and design reviews, where many can render at once.
footerReactNodeButtons, right-aligned. Primary last, and one gradient primary at most.
footerStartReactNodeLeft side of the footer: a line of secondary text, a link, a Back button.
size"sm" | "md" | "lg""md"sm 480, md 640, lg 880 (--modal-width-*). Under 640px every size becomes a bottom sheet.
mobile"sheet" | "fullscreen""sheet"Under 640px: a bottom sheet (default) or the full screen, for long forms.
toneModalTone"default"Tints the header wash and the icon tile. danger, success and info bring their own icon; pass icon={false} to drop it.
iconReactNode
progress{ step: number; of: number }A multi-step flow: draws the segmented step rule under the heading.
onScrollEnd() => voidCalled when the body has been scrolled to its end (and at once if it does not overflow). Gate a terms "Accept" on it: see the scroll-gated accept example.
dismissiblebooleantruefalse while a request is in flight: Esc, scrim and close are ignored.
busybooleanfalseA request is in flight: sets aria-busy and locks dismissal.
initialFocusRefObject<HTMLElement | null>Focused on open. Defaults to the first field or button in the body.
closeLabelstring"Close"
classNamestring

ConfirmModal

ConfirmModal props
PropTypeDefaultDescription
openRequiredboolean
onCloseRequired() => void
titleRequiredReactNode
metastringA quiet line under the title; two-step flows prefix it with "Step 1 of 2".
descriptionReactNode
inlinebooleanRender the open dialog in place (see Modal's inline).
childrenReactNodeStep one: the details or form (e.g. withdrawal reason).
reviewReactNodeStep two: what will happen, stated plainly. When given, the first button reads proceedLabel and leads here; the destructive button is only on this step. Omit for a single-step confirm.
reviewTitleReactNode
reviewDescriptionReactNodeReplaces description on step two. Omitted: step two has none.
proceedLabelstring"Continue"
confirmLabelRequiredstring
cancelLabelstring"Cancel"
toneModalTone"default"
iconReactNodeOverrides the tone's icon tile; false drops it.
onProceed() => booleanReturn false to stay on step one (failed validation).
onConfirmRequired() => void
busybooleanThe confirm request is in flight. Locks the dialog.
busyLabelstring
sizeModalProps["size"]"md"
mobileModalProps["mobile"]

Theme tokens

Per-component tokens are undeclared by default, so the default shown is the fallback the CSS uses. Set one at :root (or in theme.css) to restyle every instance. Live values are read from this page, so they follow the Rime Studio.

Modal per-component tokens
TokenDefaultLive value
--modal-inline-width520px…
--modal-inline-width-lg640px…
--modal-pad-xcalc(24px * var(--density))…
--modal-radiusvar(--r-xl)…
--modal-radius-lgvar(--r-2xl)…
--modal-width-lg880px…
--modal-width-md640px…
--modal-width-sm480px…

System tokens it reads

Modal system tokens
TokenDefaultLive value
--brand-gradienttokens.css…
--danger-gradienttokens.css…
--danger-inktokens.css…
--densitytokens.css…
--dur-slowtokens.css…
--easetokens.css…
--ease-outtokens.css…
--focus-colortokens.css…
--font-displaytokens.css…
--info-inktokens.css…
--ink-bodytokens.css…
--ink-brandtokens.css…
--ink-headingtokens.css…
--ink-mutedtokens.css…
--ink-secondarytokens.css…
--modal-blurtokens.css…
--modal-scrimtokens.css…
--modal-scrim-blurtokens.css…
--modal-shadowtokens.css…
--modal-surfacetokens.css…
--modal-wash-opacitytokens.css…
--r-2xltokens.css…
--r-fulltokens.css…
--r-mdtokens.css…
--r-xltokens.css…
--rgb-brandtokens.css…
--rgb-brand-deeptokens.css…
--rgb-brand-softtokens.css…
--rgb-dangertokens.css…
--scroll-edge-bottomtokens.css…
--scroll-edge-lefttokens.css…
--scroll-edge-righttokens.css…
--scroll-edge-toptokens.css…
--scrollbar-thumbtokens.css…
--scrollbar-tracktokens.css…
--scrollbar-widthtokens.css…
--shadow-speculartokens.css…
--success-gradienttokens.css…
--success-inktokens.css…
--success-solidtokens.css…
--text-bodytokens.css…
--text-captiontokens.css…
--text-smalltokens.css…
--tracking-displaytokens.css…
--z-modaltokens.css…

Accessibility

  • role="dialog" ("alertdialog" for tone="danger") with aria-modal, aria-labelledby on the title and aria-describedby on the description.
  • Focus trap with Tab and Shift+Tab, Esc to close, focus returned to the opener; the background is inert, except live regions (toasts) so failures are still announced.
  • busy locks dismissal and sets aria-busy while a request is in flight.
  • Reduced motion: everything fades in place; nothing travels or scales.

Do and don't

Do

  • Keep one primary action, last in the footer.
  • Use ConfirmModal for any irreversible action; TypeToConfirmModal when the blast radius is large.
  • Use footerStart for a line of context or a Back button.

Don't

  • Don't stack modals unless the second is a confirm of the first.
  • Don't put a coloured side bar or a filled header on a modal; tone is a soft wash only.
  • Don't open a modal on page load.

For AI agents

This page as markdown, rendered from the same registry entry. Point an agent at it (or add .md to this page's URL), or at /llms.txt for the index.

Open modal.md

Related: button, scroll-area, type-to-confirm