All pages

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.