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 Dialog

Install 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"
2
3import 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"
7
8// Context
9type DialogContextType = {
10 open: boolean
11 onOpenChange: (open: boolean) => void
12}
13
14const DialogContext = createContext<DialogContextType | undefined>(undefined)
15
16function useDialog() {
17 const context = useContext(DialogContext)
18 if (!context) {
19 throw new Error("Dialog components must be used within a Dialog")
20 }
21 return context
22}
23
24// Main Dialog
25type DialogProps = {
26 children: ReactNode
27 open?: boolean
28 defaultOpen?: boolean
29 onOpenChange?: (open: boolean) => void
30}
31
32export function Dialog({
33 children,
34 open: controlledOpen,
35 defaultOpen = false,
36 onOpenChange,
37}: DialogProps) {
38 const [internalOpen, setInternalOpen] = useState(defaultOpen)
39
40 const isControlled = controlledOpen !== undefined
41 const open = isControlled ? controlledOpen : internalOpen
42
43 const handleOpenChange = useCallback((newOpen: boolean) => {
44 if (!isControlled) {
45 setInternalOpen(newOpen)
46 }
47 onOpenChange?.(newOpen)
48 }, [isControlled, onOpenChange])
49
50 return (
51 <DialogContext.Provider value={{ open, onOpenChange: handleOpenChange }}>
52 {children}
53 </DialogContext.Provider>
54 )
55}
56
57// Trigger
58type DialogTriggerProps = {
59 children: ReactNode
60 className?: string
61 asChild?: boolean
62} & ButtonHTMLAttributes<HTMLButtonElement>
63
64export function DialogTrigger({ children, className, asChild, ...props }: DialogTriggerProps) {
65 const { onOpenChange } = useDialog()
66
67 if (asChild) {
68 return (
69 <span onClick={() => onOpenChange(true)} className={className}>
70 {children}
71 </span>
72 )
73 }
74
75 return (
76 <button
77 type="button"
78 className={className}
79 onClick={() => onOpenChange(true)}
80 {...props}
81 >
82 {children}
83 </button>
84 )
85}
86
87// Portal
88type DialogPortalProps = {
89 children: ReactNode
90}
91
92function DialogPortal({ children }: DialogPortalProps) {
93 const { open } = useDialog()
94
95 if (!open) return null
96
97 return <>{children}</>
98}
99
100// Overlay
101type DialogOverlayProps = {
102 className?: string
103} & HTMLAttributes<HTMLDivElement>
104
105export function DialogOverlay({ className, ...props }: DialogOverlayProps) {
106 const { open, onOpenChange } = useDialog()
107
108 return (
109 <div
110 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 className
115 )}
116 data-state={open ? "open" : "closed"}
117 onClick={() => onOpenChange(false)}
118 {...props}
119 />
120 )
121}
122
123// Content
124type DialogContentProps = {
125 children: ReactNode
126 className?: string
127} & HTMLAttributes<HTMLDivElement>
128
129export function DialogContent({ children, className, ...props }: DialogContentProps) {
130 const { open, onOpenChange } = useDialog()
131
132 // Handle escape key
133 useEffect(() => {
134 const handleEscape = (e: KeyboardEvent) => {
135 if (e.key === "Escape") {
136 onOpenChange(false)
137 }
138 }
139
140 if (open) {
141 document.addEventListener("keydown", handleEscape)
142 document.body.style.overflow = "hidden"
143 }
144
145 return () => {
146 document.removeEventListener("keydown", handleEscape)
147 document.body.style.overflow = ""
148 }
149 }, [open, onOpenChange])
150
151 return (
152 <DialogPortal>
153 <DialogOverlay />
154 <div
155 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 className
166 )}
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}
180
181// Header
182type DialogHeaderProps = {
183 children: ReactNode
184 className?: string
185} & HTMLAttributes<HTMLDivElement>
186
187export function DialogHeader({ children, className, ...props }: DialogHeaderProps) {
188 return (
189 <div
190 className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
191 {...props}
192 >
193 {children}
194 </div>
195 )
196}
197
198// Footer
199type DialogFooterProps = {
200 children: ReactNode
201 className?: string
202} & HTMLAttributes<HTMLDivElement>
203
204export function DialogFooter({ children, className, ...props }: DialogFooterProps) {
205 return (
206 <div
207 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}
214
215// Title
216type DialogTitleProps = {
217 children: ReactNode
218 className?: string
219} & HTMLAttributes<HTMLHeadingElement>
220
221export function DialogTitle({ children, className, ...props }: DialogTitleProps) {
222 return (
223 <h2
224 className={cn("text-lg font-semibold leading-none tracking-tight", className)}
225 {...props}
226 >
227 {children}
228 </h2>
229 )
230}
231
232// Description
233type DialogDescriptionProps = {
234 children: ReactNode
235 className?: string
236} & HTMLAttributes<HTMLParagraphElement>
237
238export function DialogDescription({ children, className, ...props }: DialogDescriptionProps) {
239 return (
240 <p
241 className={cn("text-sm text-muted-foreground", className)}
242 {...props}
243 >
244 {children}
245 </p>
246 )
247}
248
249// Close button
250type DialogCloseProps = {
251 children?: ReactNode
252 className?: string
253 asChild?: boolean
254} & ButtonHTMLAttributes<HTMLButtonElement>
255
256export function DialogClose({ children, className, onClick, asChild, ...props }: DialogCloseProps) {
257 const { onOpenChange } = useDialog()
258
259 const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
260 onClick?.(e)
261 onOpenChange(false)
262 }
263
264 if (asChild) {
265 return (
266 <span onClick={() => onOpenChange(false)} className={className}>
267 {children}
268 </span>
269 )
270 }
271
272 return (
273 <button
274 type="button"
275 className={className}
276 onClick={handleClick}
277 {...props}
278 >
279 {children}
280 </button>
281 )
282}
283

Usages

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>'