Components
Dialog
A decision that needs the page stopped: a restrained scrim, trapped focus, and Escape or an outside click to leave.
Examples
import { DialogRoot, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter } from "@luc/ui";Title, description, body, footer
The close affordance is always present and always labelled; the body is where the work happens.
<DialogRoot>
<DialogTrigger asChild>
<Button variant="primary" leadingIcon={<RiPlayLine />}>New run</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader icon={<RiPlayLine />}>
<DialogTitle>Start a backtest run</DialogTitle>
<DialogDescription>Runs against the retained hourly candle set.</DialogDescription>
</DialogHeader>
<DialogBody>
<Field label="Strategy" required>
<Input defaultValue="btc_sma_dca" />
</Field>
</DialogBody>
<DialogFooter>
<Button variant="ghost">Cancel</Button>
<Button variant="primary">Start run</Button>
</DialogFooter>
</DialogContent>
</DialogRoot>A narrow confirm
`width="sm"` for a decision with no body — the button that opens it is already the sentence.
<DialogRoot>
<DialogTrigger asChild>
<Button variant="danger" leadingIcon={<RiDeleteBinLine />}>Delete run</Button>
</DialogTrigger>
<DialogContent width="sm">
<DialogHeader icon={<RiDeleteBinLine />}>
<DialogTitle>Delete this run?</DialogTitle>
<DialogDescription>The report and its machine artifacts are removed.</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="ghost">Cancel</Button>
<Button variant="danger">Delete run</Button>
</DialogFooter>
</DialogContent>
</DialogRoot>A long body scrolls inside
`width="lg"` with a `max-h` on the body: the page behind stays locked and the header and footer stay put.
<DialogContent width="lg">
<DialogHeader>
<DialogTitle>Data provenance</DialogTitle>
<DialogDescription>Where the bars in this run actually came from.</DialogDescription>
</DialogHeader>
<DialogBody className="max-h-[46vh] overflow-y-auto">
{/* long content */}
</DialogBody>
<DialogFooter>…</DialogFooter>
</DialogContent>Behaviour
What the foundation guarantees, from one component.
Focus
Focus moves into the dialog on open, is trapped while open, and returns to the trigger on close.
Dismissal
Escape and outside-click close it. Interactive content inside does not.
Scroll
The page behind is locked; long bodies scroll inside `DialogBody` with a fixed header and footer.
Motion
200ms entrance, 140ms exit. Under reduced motion both collapse to a fade.
Labelling
`DialogTitle` and `DialogDescription` are wired to the dialog's ARIA attributes automatically.
Layering
Scrim at z-overlay, content at z-modal. Nothing in a page can sit above a dialog by accident.
API
| Prop | Type | Default | Notes |
|---|---|---|---|
DialogContent width | "sm" | "md" | "lg" | "md" | Max width; the dialog is always inset 1rem from the viewport. |
DialogContent showClose | boolean | true | Built-in, labelled close button. |
DialogHeader icon | ReactNode | — | Optional 32px icon tile for orientation. |
DialogRoot open / onOpenChange | boolean / (open: boolean) => void | — | Controlled usage; uncontrolled by default. |
DialogTrigger asChild | boolean | — | Attach the trigger to any focusable element. |
Rules
Do
- Use a dialog for a decision that needs context, not for every edit.
- Put the primary action last in the footer.
- Keep the title short enough to read in one glance.
Don't
- Open a dialog from inside another dialog.
- Hide the only way to continue behind a scroll.
- Use
alert()or a browser-native confirm.
Usage
import { DialogRoot, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogBody, DialogFooter, DialogClose } from "@luc/ui/components/dialog";<DialogRoot>
<DialogTrigger asChild>
<Button>Open run detail</Button>
</DialogTrigger>
<DialogContent width="md">
<DialogHeader>
<DialogTitle>Run detail</DialogTitle>
</DialogHeader>
<DialogBody>…</DialogBody>
<DialogFooter>
<DialogClose asChild>
<Button variant="ghost">Close</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</DialogRoot>Guidelines
Do
- Use a modal dialog for a task that needs the page's context but not its controls, and put the dialog's own next step in the footer.
- Set `width` rather than a `className` on the content; the width lands on the panel, while a class lands on the centring frame.
Don't
- Open a dialog from a dialog when a second step would do; nested modals double the focus trap and the escape path.
Accessibility
`DialogContent` is a modal: it traps Tab, closes on Escape, restores focus to the trigger, and locks the page behind it. `DialogTitle` is required for the accessible name; without it the dialog announces as “dialog”.
Related