Dependencies
tailwindcssshadcn/uilucide-react
shadcn/ui components
npx shadcn@latest add dialognpx shadcn@latest add buttonnpx shadcn@latest add inputnpx shadcn@latest add label

shadcn/ui Dialog provides a fully accessible modal implementation built on Radix UI. These 4 examples cover the most common modal patterns in React applications.

Installation

npx shadcn@latest add dialog button input label

Basic Usage

import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"

<Dialog>
  <DialogTrigger asChild>
    <Button>Open</Button>
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Dialog Title</DialogTitle>
    </DialogHeader>
    Content goes here
  </DialogContent>
</Dialog>

Key Props

PropTypeDescription
openbooleanControlled open state
onOpenChange(open: boolean) => voidCalled when open state changes
defaultOpenbooleanUncontrolled initial state
modalbooleanWhether dialog is modal (default true)

1. Basic shadcn/ui Dialog

Simple dialog with title, description and close/action buttons.

tsx
import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"

export default function BasicDialog() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button style={{ background: "#fd4766" }}>Open Dialog</Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w-md">
        <DialogHeader>
          <DialogTitle>About BootstrapPlanet</DialogTitle>
          <DialogDescription>
            Free Bootstrap 5, React and Angular components with live previews.
          </DialogDescription>
        </DialogHeader>
        <div className="py-3">
          <p className="text-sm text-muted-foreground">
            200+ components with copy-paste code. No signup required.
            Built for developers who want to ship faster.
          </p>
        </div>
        <DialogFooter>
          <Button variant="outline">Cancel</Button>
          <Button style={{ background: "#fd4766" }}>
            Browse Components →
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

2. React Form Modal

Dialog with a form inside. Includes controlled inputs and submit handler.

tsx
import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Plus } from "lucide-react"

export default function FormDialog() {
  const [open, setOpen] = useState(false)
  const [name, setName] = useState("")
  const [email, setEmail] = useState("")

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    if (!name || !email) return
    console.log("Submitted:", { name, email })
    setOpen(false)
    setName("")
    setEmail("")
  }

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>
        <Button style={{ background: "#fd4766" }}>
          <Plus size={16} className="mr-2" />
          Add User
        </Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w-md">
        <DialogHeader>
          <DialogTitle className="text-xl font-bold">Add New User</DialogTitle>
        </DialogHeader>
        <form onSubmit={handleSubmit} className="space-y-4 py-2">
          <div className="space-y-1.5">
            <Label htmlFor="name">Full Name</Label>
            <Input
              id="name"
              placeholder="Gagan Singh"
              value={name}
              onChange={(e) => setName(e.target.value)}
              required
            />
          </div>
          <div className="space-y-1.5">
            <Label htmlFor="email">Email Address</Label>
            <Input
              id="email"
              type="email"
              placeholder="gagan@example.com"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              required
            />
          </div>
          <div className="space-y-1.5">
            <Label htmlFor="role">Role</Label>
            <select id="role" className="w-full border rounded-md px-3 py-2 text-sm">
              <option>Admin</option>
              <option>Editor</option>
              <option>Viewer</option>
            </select>
          </div>
          <div className="flex gap-2 pt-2">
            <Button type="button" variant="outline" className="flex-1" onClick={() => setOpen(false)}>
              Cancel
            </Button>
            <Button type="submit" className="flex-1" style={{ background: "#fd4766" }}>
              Add User
            </Button>
          </div>
        </form>
      </DialogContent>
    </Dialog>
  )
}

3. Confirmation Dialog

Delete confirmation dialog with destructive button styling.

tsx
import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog"
import { Trash2, AlertTriangle } from "lucide-react"

interface ConfirmDialogProps {
  title: string
  description: string
  onConfirm: () => void
  trigger?: React.ReactNode
}

export function ConfirmDialog({ title, description, onConfirm, trigger }: ConfirmDialogProps) {
  const [open, setOpen] = useState(false)

  const handleConfirm = () => {
    onConfirm()
    setOpen(false)
  }

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <div onClick={() => setOpen(true)} className="cursor-pointer inline-block">
        {trigger || <Button variant="destructive" size="sm"><Trash2 size={14} /></Button>}
      </div>
      <DialogContent className="sm:max-w-sm">
        <DialogHeader className="text-center sm:text-center">
          <div className="mx-auto w-12 h-12 rounded-full bg-red-50 flex items-center justify-center mb-3">
            <AlertTriangle className="text-red-500" size={24} />
          </div>
          <DialogTitle>{title}</DialogTitle>
          <DialogDescription className="mt-1">{description}</DialogDescription>
        </DialogHeader>
        <DialogFooter className="flex gap-2 sm:flex-row-reverse">
          <Button
            variant="destructive"
            onClick={handleConfirm}
            className="flex-1 sm:flex-none"
          >
            Delete
          </Button>
          <Button
            variant="outline"
            onClick={() => setOpen(false)}
            className="flex-1 sm:flex-none"
          >
            Cancel
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

// Usage example
export default function ConfirmDialogExample() {
  const [deleted, setDeleted] = useState(false)

  return (
    <div className="flex items-center gap-4">
      <ConfirmDialog
        title="Delete Template"
        description="This will permanently delete Marvel Dashboard. This action cannot be undone."
        onConfirm={() => setDeleted(true)}
        trigger={
          <Button variant="destructive" size="sm">
            <Trash2 size={14} className="mr-2" /> Delete Template
          </Button>
        }
      />
      {deleted && <p className="text-sm text-muted-foreground">Template deleted ✓</p>}
    </div>
  )
}

4. React Image Preview Modal

Click an image to open a full-size preview in a dialog.

tsx
import { useState } from "react"
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { X, ZoomIn } from "lucide-react"

const images = [
  { src: "https://picsum.photos/800/500?random=20", thumb: "https://picsum.photos/300/200?random=20", alt: "Marvel Dashboard" },
  { src: "https://picsum.photos/800/500?random=21", thumb: "https://picsum.photos/300/200?random=21", alt: "PORTO Template" },
  { src: "https://picsum.photos/800/500?random=22", thumb: "https://picsum.photos/300/200?random=22", alt: "Proctu Medical" },
]

export default function ImagePreviewModal() {
  const [selected, setSelected] = useState<typeof images[0] | null>(null)

  return (
    <>
      <div className="grid grid-cols-3 gap-3">
        {images.map((img) => (
          <div
            key={img.src}
            className="relative group cursor-pointer overflow-hidden rounded-lg"
            onClick={() => setSelected(img)}
          >
            <img
              src={img.thumb}
              alt={img.alt}
              className="w-full h-32 object-cover transition-transform duration-300 group-hover:scale-110"
            />
            <div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
              <ZoomIn className="text-white" size={24} />
            </div>
          </div>
        ))}
      </div>

      <Dialog open={!!selected} onOpenChange={(open) => !open && setSelected(null)}>
        <DialogContent className="max-w-3xl p-2">
          {selected && (
            <>
              <img
                src={selected.src}
                alt={selected.alt}
                className="w-full rounded-lg"
              />
              <p className="text-center text-sm text-muted-foreground mt-2 pb-1">
                {selected.alt}
              </p>
            </>
          )}
        </DialogContent>
      </Dialog>
    </>
  )
}

Frequently Asked Questions

Use controlled mode with open and onOpenChange props: const [open, setOpen] = useState(false). Pass <Dialog open={open} onOpenChange={setOpen}>. Call setOpen(true) from anywhere to open programmatically. Call setOpen(false) to close.
Add onInteractOutside to DialogContent: <DialogContent onInteractOutside={(e) => e.preventDefault()}>. This stops the dialog closing when clicking outside. Users must use the close button or keyboard Escape.
Override the DialogContent size classes: <DialogContent className='max-w-full h-full m-0 rounded-none sm:rounded-none'>. Or use shadcn/ui Sheet component which slides in from an edge — better UX than a full-screen dialog on mobile.
Yes but use with caution. Each Dialog needs its own state. Focus trapping can get complex with nested dialogs. For complex flows consider a multi-step wizard inside a single dialog rather than nested dialogs.

Need a Full React + Next.js Dashboard Template?

Get a complete React + Next.js dashboard with 50+ components — built by the same team behind BootstrapPlanet.

Browse Templates →

Use code FIRST30 for 30% off.

Related Components