Skip to Content
Guidesshadcn/ui

shadcn/ui

Wire @lamstack/react-dialog’s templates to shadcn/ui’s Dialog (a styled wrapper around Radix UI’s dialog primitive). The component below was generated with the real shadcn CLI (npx shadcn@latest add dialog) — its exact markup/class names come from the registry, not hand-written from memory.

Live demo

Try all four ways to open a dialog — the log below shows exactly what each call resolved with:

  • alert() — acknowledgement only, resolves void once dismissed.
  • confirm() — resolves true/false depending on which button was pressed.
  • prompt() — resolves the typed string, or null if cancelled.
  • open(custom) — opens RatingDialog, built from the same shadcn Dialog primitives plus a lucide-react star icon, with its own payload/result type ({ itemName: string }number | null) that has nothing to do with the three built-in kinds above.
alert()
Acknowledgement only — resolves void once dismissed.
confirm()
Resolves true/false depending on which button was pressed.
prompt()
Resolves the typed string, or null if cancelled.
open(custom)
Any component works, not just alert/confirm/prompt — here, a rating.
Chaining dialogs with async/await
Each call blocks until its dialog closes, so a multi-step flow — confirm, then prompt, then alert — reads top-to-bottom. No nested callbacks, no extra state for tracking which dialog is currently open.
Loading state via async onClose()
onClose runs before the promise resolves — the dialog awaits it itself to show “Saving…” with no extra state passed in from here.

Action buttons call onClose(result) directly rather than being wrapped in shadcn’s <DialogClose> — that would fire onOpenChange(false) and the button’s own onClick, resolving the promise twice with two different results.

A custom (non-template) dialog

The open(custom) button above opens this — same shadcn Dialog primitives, but a component with its own payload/result types instead of one of the three built-in kinds:

import * as React from 'react'; import { Star } from 'lucide-react'; import { useDialogs } from '@lamstack/react-dialog'; import type { DialogProps } from '@lamstack/react-dialog'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; interface ItemPayload { itemName: string; } function RatingDialog({ payload, open, onClose }: DialogProps<ItemPayload, number | null>) { const [rating, setRating] = React.useState(0); return ( <Dialog open={open} onOpenChange={(next) => !next && onClose(null)}> <DialogContent> <DialogHeader> <DialogTitle>Rate &ldquo;{payload.itemName}&rdquo;</DialogTitle> </DialogHeader> <div className="flex gap-1"> {[1, 2, 3, 4, 5].map((star) => ( <button key={star} onClick={() => setRating(star)}> <Star className={star <= rating ? 'fill-primary text-primary' : 'text-muted-foreground'} size={20} /> </button> ))} </div> <DialogFooter> <Button variant="outline" onClick={() => onClose(null)}> Skip </Button> <Button disabled={rating === 0} onClick={() => onClose(rating)}> Submit </Button> </DialogFooter> </DialogContent> </Dialog> ); } function RateButton() { const { open } = useDialogs(); return ( <Button onClick={async () => console.log(await open(RatingDialog, { itemName: 'Acme Widget' }))} > Rate this </Button> ); }

Setup

npx shadcn@latest init npx shadcn@latest add dialog

The CLI needs a resolvable import alias (@/*) in tsconfig.json — if it fails with “Could not find valid path aliases”, add one:

{ "compilerOptions": { "paths": { "@/*": ["./*"] } } }

Dark mode

shadcn’s generated tailwind.css defines :root and .dark CSS variable blocks (colors as oklch() values) — it already assumes class-based dark mode, matching this docs site’s .dark class toggle with no extra configuration needed (unlike the MUI guide, which needed colorSchemeSelector: 'class' set explicitly).

Last updated on