Dialog
A modal dialog that opens in the center of the screen, used to display content that requires user attention.
Installation
Install the component Dialog in your project using the CLI.
Dialog.tsx
pnpm dlx behsseui@latest add DialogInstall the component manually.
Create a ui folder at the root of the project, then a component folder inside it, and finally a Dialog.tsx file in that folder.
Copy and paste the following code into your project.
ui/components/Dialog.tsx
1"use client"23import type { ReactNode, HTMLAttributes, ButtonHTMLAttributes } from "react"4import { createContext, useContext, useState, useEffect, useCallback } from "react"5import { cn } from "@/lib/utils"6import Close from "@/ui/icons/Close"78// Context9type DialogContextType = {10 open: boolean11 onOpenChange: (open: boolean) => void12}1314const DialogContext = createContext<DialogContextType | undefined>(undefined)1516function useDialog() {17 const context = useContext(DialogContext)18 if (!context) {19 throw new Error("Dialog components must be used within a Dialog")20 }21 return context22}2324// Main Dialog25type DialogProps = {26 children: ReactNode27 open?: boolean28 defaultOpen?: boolean29 onOpenChange?: (open: boolean) => void30}3132export function Dialog({33 children,34 open: controlledOpen,35 defaultOpen = false,36 onOpenChange,37}: DialogProps) {38 const [internalOpen, setInternalOpen] = useState(defaultOpen)3940 const isControlled = controlledOpen !== undefined41 const open = isControlled ? controlledOpen : internalOpen4243 const handleOpenChange = useCallback((newOpen: boolean) => {44 if (!isControlled) {45 setInternalOpen(newOpen)46 }47 onOpenChange?.(newOpen)48 }, [isControlled, onOpenChange])4950 return (51 <DialogContext.Provider value={{ open, onOpenChange: handleOpenChange }}>52 {children}53 </DialogContext.Provider>54 )55}5657// Trigger58type DialogTriggerProps = {59 children: ReactNode60 className?: string61 asChild?: boolean62} & ButtonHTMLAttributes<HTMLButtonElement>6364export function DialogTrigger({ children, className, asChild, ...props }: DialogTriggerProps) {65 const { onOpenChange } = useDialog()6667 if (asChild) {68 return (69 <span onClick={() => onOpenChange(true)} className={className}>70 {children}71 </span>72 )73 }7475 return (76 <button77 type="button"78 className={className}79 onClick={() => onOpenChange(true)}80 {...props}81 >82 {children}83 </button>84 )85}8687// Portal88type DialogPortalProps = {89 children: ReactNode90}9192function DialogPortal({ children }: DialogPortalProps) {93 const { open } = useDialog()9495 if (!open) return null9697 return <>{children}</>98}99100// Overlay101type DialogOverlayProps = {102 className?: string103} & HTMLAttributes<HTMLDivElement>104105export function DialogOverlay({ className, ...props }: DialogOverlayProps) {106 const { open, onOpenChange } = useDialog()107108 return (109 <div110 className={cn(111 "fixed inset-0 z-50 bg-black/80",112 "data-[state=open]:animate-in data-[state=closed]:animate-out",113 "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",114 className115 )}116 data-state={open ? "open" : "closed"}117 onClick={() => onOpenChange(false)}118 {...props}119 />120 )121}122123// Content124type DialogContentProps = {125 children: ReactNode126 className?: string127} & HTMLAttributes<HTMLDivElement>128129export function DialogContent({ children, className, ...props }: DialogContentProps) {130 const { open, onOpenChange } = useDialog()131132 // Handle escape key133 useEffect(() => {134 const handleEscape = (e: KeyboardEvent) => {135 if (e.key === "Escape") {136 onOpenChange(false)137 }138 }139140 if (open) {141 document.addEventListener("keydown", handleEscape)142 document.body.style.overflow = "hidden"143 }144145 return () => {146 document.removeEventListener("keydown", handleEscape)147 document.body.style.overflow = ""148 }149 }, [open, onOpenChange])150151 return (152 <DialogPortal>153 <DialogOverlay />154 <div155 role="dialog"156 aria-modal="true"157 className={cn(158 "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%]",159 "gap-4 border bg-background p-6 shadow-lg duration-200 sm:rounded-lg",160 "data-[state=open]:animate-in data-[state=closed]:animate-out",161 "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",162 "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",163 "data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]",164 "data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",165 className166 )}167 data-state={open ? "open" : "closed"}168 onClick={(e) => e.stopPropagation()}169 {...props}170 >171 {children}172 <DialogClose className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">173 <Close className="h-4 w-4 fill-foreground" />174 <span className="sr-only">Close</span>175 </DialogClose>176 </div>177 </DialogPortal>178 )179}180181// Header182type DialogHeaderProps = {183 children: ReactNode184 className?: string185} & HTMLAttributes<HTMLDivElement>186187export function DialogHeader({ children, className, ...props }: DialogHeaderProps) {188 return (189 <div190 className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}191 {...props}192 >193 {children}194 </div>195 )196}197198// Footer199type DialogFooterProps = {200 children: ReactNode201 className?: string202} & HTMLAttributes<HTMLDivElement>203204export function DialogFooter({ children, className, ...props }: DialogFooterProps) {205 return (206 <div207 className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}208 {...props}209 >210 {children}211 </div>212 )213}214215// Title216type DialogTitleProps = {217 children: ReactNode218 className?: string219} & HTMLAttributes<HTMLHeadingElement>220221export function DialogTitle({ children, className, ...props }: DialogTitleProps) {222 return (223 <h2224 className={cn("text-lg font-semibold leading-none tracking-tight", className)}225 {...props}226 >227 {children}228 </h2>229 )230}231232// Description233type DialogDescriptionProps = {234 children: ReactNode235 className?: string236} & HTMLAttributes<HTMLParagraphElement>237238export function DialogDescription({ children, className, ...props }: DialogDescriptionProps) {239 return (240 <p241 className={cn("text-sm text-muted-foreground", className)}242 {...props}243 >244 {children}245 </p>246 )247}248249// Close button250type DialogCloseProps = {251 children?: ReactNode252 className?: string253 asChild?: boolean254} & ButtonHTMLAttributes<HTMLButtonElement>255256export function DialogClose({ children, className, onClick, asChild, ...props }: DialogCloseProps) {257 const { onOpenChange } = useDialog()258259 const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {260 onClick?.(e)261 onOpenChange(false)262 }263264 if (asChild) {265 return (266 <span onClick={() => onOpenChange(false)} className={className}>267 {children}268 </span>269 )270 }271272 return (273 <button274 type="button"275 className={className}276 onClick={handleClick}277 {...props}278 >279 {children}280 </button>281 )282}283Usages
Different variants and use cases for the Dialog component.
Default
A basic dialog with a title, description, and action buttons.
Default.tsx
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>
This is a dialog description that explains what this dialog is about.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button>Continue</Button>
</DialogFooter>
</DialogContent>
</Dialog>With Form
A dialog containing a form for user input.
With Form.tsx
<Dialog>
<DialogTrigger asChild>
<Button>Edit Profile</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="name" className="text-right text-sm">Name</label>
<input id="name" defaultValue="John Doe" className="col-span-3 h-9 rounded-md border border-input bg-background px-3 text-sm" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="email" className="text-right text-sm">Email</label>
<input id="email" defaultValue="john@example.com" className="col-span-3 h-9 rounded-md border border-input bg-background px-3 text-sm" />
</div>
</div>
<DialogFooter>
<Button>Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>'