Dialog Component
A modal overlay component that displays content on top of the main application window, rendering the underlying content inert until the dialog is dismissed.
Import
import {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogClose
} from '@/components/ui/dialog';
Usage
Basic Usage
<Dialog>
  <DialogTrigger>Open Dialog</DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Dialog Title</DialogTitle>
      <DialogDescription>This is a description of the dialog content.</DialogDescription>
    </DialogHeader>
    <p>Main content goes here.</p>
    <DialogFooter>
      <Button variant="outline" onClick={() => {}}>Cancel</Button>
      <Button onClick={() => {}}>Save</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>
Custom Trigger
<Dialog>
  <DialogTrigger asChild>
    <Button variant="outline">Custom Trigger Button</Button>
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Custom Trigger</DialogTitle>
      <DialogDescription>A dialog triggered by a custom button element.</DialogDescription>
    </DialogHeader>
    <p>You can use any component as a trigger with the asChild prop.</p>
  </DialogContent>
</Dialog>
Custom Close Button
<Dialog>
  <DialogTrigger>Open Dialog</DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Custom Close</DialogTitle>
      <DialogDescription>This dialog has a custom close button.</DialogDescription>
    </DialogHeader>
    <div className="py-4">Dialog content here.</div>
    <DialogFooter>
      <DialogClose asChild>
        <Button variant="outline">Close Dialog</Button>
      </DialogClose>
    </DialogFooter>
  </DialogContent>
</Dialog>
Controlled Dialog
'use client';
import { useState } from 'react';
export function ControlledDialog() {
  const [open, setOpen] = useState(false);
  
  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger>Open Controlled Dialog</DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Controlled Dialog</DialogTitle>
          <DialogDescription>This dialog's state is controlled programmatically.</DialogDescription>
        </DialogHeader>
        <p>You can control the open state with your own state management.</p>
        <DialogFooter>
          <Button onClick={() => setOpen(false)}>Close</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}
Props
Dialog Props
| Prop | Type | Default | Description | 
|---|---|---|---|
open | boolean | undefined | Controls the open state when used as a controlled component | 
defaultOpen | boolean | false | The default open state when uncontrolled | 
onOpenChange | (open: boolean) => void | undefined | Callback fired when the open state changes | 
modal | boolean | true | Whether to render as a modal dialog | 
children | ReactNode | Required | The dialog trigger and content components | 
DialogTrigger Props
| Prop | Type | Default | Description | 
|---|---|---|---|
asChild | boolean | false | When true, the component will render its child instead of a default button | 
children | ReactNode | Required | The element that triggers the dialog | 
DialogContent Props
| Prop | Type | Default | Description | 
|---|---|---|---|
className | string | undefined | Additional CSS classes to apply to the dialog content | 
children | ReactNode | Required | The content to display inside the dialog | 
forceMount | boolean | false | Force the dialog to mount even when it's not open | 
onEscapeKeyDown | (event: KeyboardEvent) => void | undefined | Event handler called when the escape key is pressed | 
onPointerDownOutside | (event: PointerDownOutsideEvent) => void | undefined | Event handler called when a pointer event occurs outside the dialog | 
onInteractOutside | `(event: React.MouseEvent | React.TouchEvent) => void` | undefined | 
...props | React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> | - | All other props are passed to the underlying Radix UI Dialog Content | 
DialogHeader Props
| Prop | Type | Default | Description | 
|---|---|---|---|
className | string | undefined | Additional CSS classes to apply to the dialog header | 
children | ReactNode | Required | The content to display inside the dialog header | 
...props | React.HTMLAttributes<HTMLDivElement> | - | All other props are passed to the underlying div element | 
DialogFooter Props
| Prop | Type | Default | Description | 
|---|---|---|---|
className | string | undefined | Additional CSS classes to apply to the dialog footer | 
children | ReactNode | Required | The content to display inside the dialog footer | 
...props | React.HTMLAttributes<HTMLDivElement> | - | All other props are passed to the underlying div element | 
DialogTitle Props
| Prop | Type | Default | Description | 
|---|---|---|---|
className | string | undefined | Additional CSS classes to apply to the dialog title | 
children | ReactNode | Required | The content to display as the dialog title | 
...props | React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> | - | All other props are passed to the underlying Radix UI Dialog Title | 
DialogDescription Props
| Prop | Type | Default | Description | 
|---|---|---|---|
className | string | undefined | Additional CSS classes to apply to the dialog description | 
children | ReactNode | Required | The content to display as the dialog description | 
...props | React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> | - | All other props are passed to the underlying Radix UI Dialog Description | 
DialogClose Props
| Prop | Type | Default | Description | 
|---|---|---|---|
asChild | boolean | false | When true, the component will render its child instead of a default button | 
children | ReactNode | <X /> | The element used to close the dialog | 
...props | React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close> | - | All other props are passed to the underlying Radix UI Dialog Close | 
TypeScript
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
// Dialog Props
type DialogProps = React.ComponentProps<typeof DialogPrimitive.Root>;
// DialogTrigger Props
type DialogTriggerProps = React.ComponentProps<typeof DialogPrimitive.Trigger>;
// DialogContent Props
type DialogContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>;
// DialogHeader Props
type DialogHeaderProps = React.HTMLAttributes<HTMLDivElement>;
// DialogFooter Props
type DialogFooterProps = React.HTMLAttributes<HTMLDivElement>;
// DialogTitle Props
type DialogTitleProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>;
// DialogDescription Props
type DialogDescriptionProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>;
// DialogClose Props
type DialogCloseProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>;
Customization
Style Overrides
The Dialog component can be customized using the following approaches:
- Using the 
classNameprop to add additional Tailwind classes to each sub-component: 
<Dialog>
  <DialogTrigger>Open Dialog</DialogTrigger>
  <DialogContent className="max-w-md rounded-xl bg-slate-50 dark:bg-slate-900">
    <DialogHeader className="border-b pb-4">
      <DialogTitle className="text-xl text-primary">Custom Title</DialogTitle>
      <DialogDescription className="text-primary/70">
        This dialog has custom styling.
      </DialogDescription>
    </DialogHeader>
    <div className="py-4">Content with custom styling.</div>
    <DialogFooter className="border-t pt-4 gap-2">
      <Button variant="outline" className="flex-1">Cancel</Button>
      <Button className="flex-1">Save</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>
- Customizing the animation and transition properties:
 
<DialogContent
  className="data-[state=open]:animate-customFadeIn data-[state=closed]:animate-customFadeOut"
  style={{ animationDuration: '400ms' }}
>
  {/* Dialog content */}
</DialogContent>
Extending the Component
'use client';
import { ReactNode } from 'react';
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
interface ConfirmDialogProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  title: string;
  description?: string;
  confirmLabel?: string;
  cancelLabel?: string;
  onConfirm: () => void;
  onCancel?: () => void;
  children?: ReactNode;
  variant?: 'default' | 'destructive';
}
export function ConfirmDialog({
  open,
  onOpenChange,
  title,
  description,
  confirmLabel = 'Confirm',
  cancelLabel = 'Cancel',
  onConfirm,
  onCancel,
  children,
  variant = 'default'
}: ConfirmDialogProps) {
  const handleCancel = () => {
    onOpenChange(false);
    onCancel?.();
  };
  const handleConfirm = () => {
    onConfirm();
    onOpenChange(false);
  };
  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>{title}</DialogTitle>
          {description && <DialogDescription>{description}</DialogDescription>}
        </DialogHeader>
        {children}
        <DialogFooter>
          <Button variant="outline" onClick={handleCancel}>
            {cancelLabel}
          </Button>
          <Button 
            variant={variant === 'destructive' ? 'destructive' : 'default'} 
            onClick={handleConfirm}
          >
            {confirmLabel}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}
Examples
Integration with Forms
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import * as z from 'zod';
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogTrigger
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
const formSchema = z.object({
  name: z.string().min(2, { message: 'Name must be at least 2 characters' }),
  email: z.string().email({ message: 'Please enter a valid email address' }),
});
export function FormDialog() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      name: '',
      email: '',
    },
  });
  function onSubmit(values: z.infer<typeof formSchema>) {
    console.log(values);
    // Handle form submission
  }
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button>Open Form Dialog</Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w-[425px]">
        <DialogHeader>
          <DialogTitle>Edit Profile</DialogTitle>
        </DialogHeader>
        <Form {...form}>
          <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
            <FormField
              control={form.control}
              name="name"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Name</FormLabel>
                  <FormControl>
                    <Input placeholder="Enter your name" {...field} />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
            <FormField
              control={form.control}
              name="email"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Email</FormLabel>
                  <FormControl>
                    <Input placeholder="Enter your email" {...field} />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
            <div className="flex justify-end space-x-2 pt-4">
              <Button type="button" variant="outline" onClick={() => form.reset()}>
                Cancel
              </Button>
              <Button type="submit">Save</Button>
            </div>
          </form>
        </Form>
      </DialogContent>
    </Dialog>
  );
}
Integration with Other Components
'use client';
import { useState } from 'react';
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogTrigger
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Input } from '@/components/ui/input';
export function ComplexDialog() {
  const [activeTab, setActiveTab] = useState('details');
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button>View Product Options</Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w-[600px]">
        <DialogHeader>
          <DialogTitle>Product Configuration</DialogTitle>
          <DialogDescription>
            Configure product options and save your preferences.
          </DialogDescription>
        </DialogHeader>
        
        <Tabs defaultValue="details" onValueChange={setActiveTab}>
          <TabsList className="grid w-full grid-cols-2">
            <TabsTrigger value="details">Details</TabsTrigger>
            <TabsTrigger value="preferences">Preferences</TabsTrigger>
          </TabsList>
          
          <TabsContent value="details">
            <Card>
              <CardHeader>
                <CardTitle>Product Details</CardTitle>
                <CardDescription>Configure basic product information.</CardDescription>
              </CardHeader>
              <CardContent className="space-y-4">
                <div className="space-y-2">
                  <label htmlFor="name" className="text-sm font-medium">Name</label>
                  <Input id="name" placeholder="Product name" />
                </div>
                <div className="space-y-2">
                  <label htmlFor="description" className="text-sm font-medium">Description</label>
                  <Input id="description" placeholder="Product description" />
                </div>
              </CardContent>
            </Card>
          </TabsContent>
          
          <TabsContent value="preferences">
            <Card>
              <CardHeader>
                <CardTitle>User Preferences</CardTitle>
                <CardDescription>Configure your personal preferences.</CardDescription>
              </CardHeader>
              <CardContent className="space-y-4">
                <div className="space-y-2">
                  <label htmlFor="theme" className="text-sm font-medium">Theme</label>
                  <Input id="theme" placeholder="Choose theme" />
                </div>
                <div className="space-y-2">
                  <label htmlFor="language" className="text-sm font-medium">Language</label>
                  <Input id="language" placeholder="Select language" />
                </div>
              </CardContent>
            </Card>
          </TabsContent>
        </Tabs>
        
        <DialogFooter>
          <Button variant="outline">Cancel</Button>
          <Button>Save Changes</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}
Responsive Behavior
The Dialog component is responsive by default, but you can enhance its behavior across different device sizes:
<Dialog>
  <DialogTrigger>Open Dialog</DialogTrigger>
  <DialogContent className="w-[90vw] max-w-[90vw] md:w-auto md:max-w-md lg:max-w-lg">
    <DialogHeader className="text-center sm:text-left">
      <DialogTitle className="text-xl sm:text-2xl">Responsive Dialog</DialogTitle>
      <DialogDescription className="text-sm sm:text-base">
        This dialog adjusts its size and layout based on screen size.
      </DialogDescription>
    </DialogHeader>
    <div className="py-4">
      <p className="text-sm sm:text-base">
        Content will reflow and resize based on the viewport.
      </p>
    </div>
    <DialogFooter className="flex-col space-y-2 sm:flex-row sm:space-y-0 sm:space-x-2">
      <Button variant="outline" className="w-full sm:w-auto">Cancel</Button>
      <Button className="w-full sm:w-auto">Continue</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>
The responsive behavior includes:
- Mobile: Full-width dialog with stacked footer buttons
 - Tablet: Constrained width with horizontal footer buttons
 - Desktop: Larger maximum width with standard layout
 
Accessibility
The Dialog component follows these accessibility best practices:
- Implements the WAI-ARIA Dialog Pattern
 - Uses appropriate ARIA roles (
dialogandalertdialog) based on content - Traps focus within the dialog when open
 - Supports keyboard navigation (Tab, Shift+Tab) for interactive elements
 - Closes on Escape key press by default
 - Prevents interaction with content behind the dialog when open
 - Includes a visible close button with screen reader accessible label
 - Has properly associated DialogTitle and DialogDescription elements
 - Automatically manages focus restoration when the dialog is closed
 - Announces dialog content to screen readers when opened
 
Implementation Details
The component:
- Is built on Radix UI's Dialog primitive for robust accessibility and keyboard handling
 - Uses a Portal to render outside the normal DOM hierarchy, preventing stacking issues
 - Applies smooth animations for opening and closing transitions
 - Renders with a semi-transparent backdrop that prevents interaction with content underneath
 - Centers content in the viewport with proper positioning
 - Includes responsive styling using Tailwind CSS with breakpoint adaptations
 - Handles both controlled (with 
openandonOpenChange) and uncontrolled usage patterns - Includes specialized layout components (Header, Footer) for consistent structure
 - Implements a close button in the top right corner by default
 
Common Pitfalls
- Z-index conflicts: The Dialog uses 
z-50by default. If it appears behind other elements, you may need to adjust z-index values. - Scrolling behavior: Long dialog content can cause issues. Use max-height constraints and enable overflow scrolling on the content section where needed.
 - Controlled state management: When using the controlled pattern, ensure state updates are properly handled to avoid the dialog getting stuck in an open or closed state.
 - Focus management: Custom implementations should maintain proper focus handling for accessibility.
 - Using within Context Menu or Dropdown Menu: To activate a Dialog from these components, ensure you wrap the Menu component with the Dialog component as noted in the Shadcn documentation.
 - Server components: The Dialog component uses client-side features and must be used within client components or with the 'use client' directive.
 
Testing
// Example test for the Dialog component
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Dialog, DialogTrigger, DialogContent, DialogTitle } from '@/components/ui/dialog';
describe('Dialog', () => {
  it('opens when trigger is clicked', async () => {
    render(
      <Dialog>
        <DialogTrigger>Open Dialog</DialogTrigger>
        <DialogContent>
          <DialogTitle>Test Dialog</DialogTitle>
          <p>Dialog content</p>
        </DialogContent>
      </Dialog>
    );
    
    // Check that dialog is not initially in the document
    expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
    
    // Click the trigger button
    await userEvent.click(screen.getByText('Open Dialog'));
    
    // Check that dialog is now in the document
    await waitFor(() => {
      expect(screen.getByRole('dialog')).toBeInTheDocument();
      expect(screen.getByText('Test Dialog')).toBeInTheDocument();
      expect(screen.getByText('Dialog content')).toBeInTheDocument();
    });
  });
  
  it('closes when close button is clicked', async () => {
    render(
      <Dialog>
        <DialogTrigger>Open Dialog</DialogTrigger>
        <DialogContent>
          <DialogTitle>Test Dialog</DialogTitle>
          <p>Dialog content</p>
        </DialogContent>
      </Dialog>
    );
    
    // Open the dialog
    await userEvent.click(screen.getByText('Open Dialog'));
    
    // Check that dialog is open
    await waitFor(() => {
      expect(screen.getByRole('dialog')).toBeInTheDocument();
    });
    
    // Click the close button (X in the corner)
    const closeButton = screen.getByRole('button', { name: /close/i });
    await userEvent.click(closeButton);
    
    // Check that dialog is closed
    await waitFor(() => {
      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
    });
  });
  
  it('supports controlled open state', async () => {
    const ControlledDialogTest = () => {
      const [open, setOpen] = React.useState(false);
      return (
        <div>
          <button onClick={() => setOpen(true)}>Open Controlled</button>
          <Dialog open={open} onOpenChange={setOpen}>
            <DialogContent>
              <DialogTitle>Controlled Dialog</DialogTitle>
              <button onClick={() => setOpen(false)}>Close</button>
            </DialogContent>
          </Dialog>
        </div>
      );
    };
    
    render(<ControlledDialogTest />);
    
    // Open dialog with external button
    await userEvent.click(screen.getByText('Open Controlled'));
    
    // Check dialog is open
    await waitFor(() => {
      expect(screen.getByRole('dialog')).toBeInTheDocument();
    });
    
    // Close with internal button
    await userEvent.click(screen.getByText('Close'));
    
    // Check dialog is closed
    await waitFor(() => {
      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
    });
  });
});
Related Components
- AlertDialog: Similar to Dialog, but for critical confirmations that interrupt the user
 - Sheet: Extends the Dialog component to create a slide-in panel from the edge of the screen
 - Popover: For smaller, non-modal overlays that don't block the main content
 - DropdownMenu: For dropdown menus that can trigger dialogs
 - Drawer: Alternative to Dialog for mobile-friendly slide-in panels