Components
Dialog
The shadcn/Radix dialog on Curation's surfaces: a bg-background panel over
the shared scrim, arriving on the dialog pair — the scrim crossfades on its
own curve while the panel scales from 0.8 on the overlay ease.
Usage
The full Radix anatomy is exported — Dialog, DialogTrigger,
DialogContent, DialogHeader, DialogTitle, DialogDescription,
DialogFooter, DialogClose. The content draws its own hairline as
border-border explicitly: Tailwind 4 dropped the preflight rule that gave
every border a default colour, so a bare border resolves to currentColor —
on a panel whose text is ink, a hard white hairline in dark mode.
<Dialog>
<DialogTrigger asChild>
<Button variant="secondary">Rename list</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Rename list</DialogTitle>
<DialogDescription>Shown on your public profile.</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild><Button variant="outline">Cancel</Button></DialogClose>
<Button>Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>Motion
shadcn ships its dialog transition in tw-animate-css vocabulary — a plugin
this app does not install, so those classes compiled to nothing and every
dialog appeared and vanished on a single frame. The content carries the app's
own pair instead: data-[state=open]:animate-dialog-in data-[state=closed]:animate-dialog-out, legacy's fade-scale from 0.8 over
--duration-fast. Radix reads data-state and will not unmount while an
animation runs on the node — which is what lets the closed half play at all.
Mount-to-open
A dialog written <Dialog open onOpenChange={…}> is mounted to open, and
closing it by unmounting skips the exit animation — the closing frame is the
last frame. useDialogDismiss in hooks/use-dialog-exit.ts is the missing
beat: open starts true, flips false on dismiss(), and the real onClose
fires after DIALOG_EXIT_MS (200ms — --duration-fast, kept in step by
hand). Its sibling useLingeringValue holds a dialog's contents while the
exit plays, so nulling the state that opened it does not empty a panel the
user is still watching. Dialogs that close by flipping a prop need neither.
The modal layer
Above single dialogs sits the modal registry (components/modal): named
modals opened by search param, the successor to legacy's 40-entry modal
router. Every entry is code-split, and names ending in -page render
full-screen on mobile rather than overlaid — that suffix is load-bearing, not
decorative.