The interruption doctrine (components/interruptions) says when a surface may take the screen. These are the shipped primitives that enforce it — real components from @morna/ui, built on react-aria, with focus, dismissal and keyboard behavior handled by construction.
Modal
The centered ask. It exists for exactly two moments: a decision that must not be made in passing, and the stop (role="alertdialog"). Everything else belongs beside the work, not on top of it.
import { Modal } from "@morna/ui";
<Modal
title="Send the chase?"
provenance="maris" // the seat's light on the panel edge
isOpen={open} onOpenChange={setOpen}
size="sm" // "sm" | "md" | "lg"
role="dialog" // "alertdialog" for the stop
>
{({ close }) => <>…the ask, then the yes…</>}
</Modal>| Prop | Type | Notes |
|---|---|---|
isOpen / defaultOpen | boolean | controlled or uncontrolled — pick one |
onOpenChange | (isOpen) => void | fires on every open/close, including Esc and scrim |
size | "sm" | "md" | "lg" | default "md" |
role | "dialog" | "alertdialog" | alertdialog reserves itself for the stop |
isDismissable | boolean | default true — an ask can always be parked (Law vii) |
title | ReactNode | renders the header and the close button |
provenance | string | any seat id — the panel wears its author's light |
children | ReactNode | ({ close }) => ReactNode | render-prop form receives close() |
Drawer
The edge-anchored panel — review beside the work. Same contract as Modal plus side ("end" by default, the reading edge). This is the drawer that ships today; the bottom-anchored detent grammar documented under patterns/bottom-drawer is the designed-ahead mobile contract it grows into — one component, two postures, chosen by surface.
import { Drawer } from "@morna/ui";
<Drawer title="Quote for Meridian Café" provenance="rena" side="end" size="md"
isOpen={open} onOpenChange={setOpen}>
{({ close }) => <>…the review, the evidence, the yes…</>}
</Drawer>Popover & menu
Two anchored surfaces with one rule between them: rich content goes in a Popover; a list of actions goes in a Menu. The Menu brings keyboard navigation, typeahead and roving focus for free, and styles destructive items apart — which is why an action list hand-rolled inside a Popover is a review comment, not a choice.
import { Popover, Menu, IconButton } from "@morna/ui";
<Popover trigger={<IconButton name="info" label="What counts as recovered?" />}
placement="bottom start">
…explanation, a sourced figure, a link to the receipt…
</Popover>
<Menu triggerLabel="Deal actions" trigger={<IconButton name="kebab" label="Deal actions" />}
items={[
{ id: "tag", label: "Add tag", icon: "plus", onAction: addTag },
{ id: "archive", label: "Archive", icon: "delete", destructive: true, onAction: archive },
]} />| Prop | Type | Notes |
|---|---|---|
Popover · trigger | ReactNode | the anchor; triggerLabel names it for the screen reader |
Popover · placement | OverlayPlacement | "bottom start" default; top/bottom/left/right ± start/end |
Menu · triggerLabel | string (required) | a menu trigger is never unlabeled |
Menu · items | MenuAction[] | id · label · icon? · onAction? · destructive? · isDisabled? |
Choosing
Four surfaces, one decision tree — the doctrine as a table:
| Prop | Type | Notes |
|---|---|---|
Modal | a decision that must not be made in passing | the yes on something irreversible; the stop |
Drawer | review beside the work | a record's detail, an edit, evidence for a figure |
Popover | rich context on demand | explanations, previews, a peeled source |
Menu | a short list of actions | row actions, the kebab — destructive items styled apart |