Dialog

Modal dialog with backdrop blur, scroll lock, keyboard dismiss, and multiple size options — perfect for confirmations, forms, and alerts.

Preview

Confirmation dialog

import { Dialog, DialogTitle, DialogDescription, DialogActions } from '@/registry/ui-kit/tailwind-dialog/react'
import { useState } from 'react'

function Example() {
  const [open, setOpen] = useState(false)
  return (
    <>
      <button onClick={() => setOpen(true)}>Delete project</button>
      <Dialog open={open} onClose={() => setOpen(false)}>
        <DialogTitle>Delete project</DialogTitle>
        <DialogDescription>Are you sure? This action cannot be undone.</DialogDescription>
        <DialogActions>
          <button onClick={() => setOpen(false)}>Cancel</button>
          <button onClick={() => setOpen(false)}>Delete</button>
        </DialogActions>
      </Dialog>
    </>
  )
}

Component API

PropDefaultDescription
openfalseWhether the dialog is visible.
onCloseCallback fired when the dialog should close.
size"md"Max-width of the dialog panel.
DialogTitleHeading rendered inside the dialog panel.
DialogDescriptionSecondary text below the title.
DialogBodyGeneric container for dialog content such as forms.
DialogActionsRight-aligned row of action buttons.

Source Code

"use client";

import React, { useEffect, useRef } from "react";

const sizeMap: Record<string, string> = {
  xs: "max-w-xs",
  sm: "max-w-sm",
  md: "max-w-md",
  lg: "max-w-lg",
  xl: "max-w-xl",
};

/* ── Dialog ─────────────────────────────────────────── */
export function Dialog({
  open,
  onClose,
  size = "md",
  className = "",
  children,
}: {
  open: boolean;
  onClose: (value: false) => void;
  size?: "xs" | "sm" | "md" | "lg" | "xl";
  className?: string;
  children: React.ReactNode;
}) {
  const panelRef = useRef<HTMLDivElement>(null);

  // Escape key
  useEffect(() => {
    function handleKey(e: KeyboardEvent) {
      if (e.key === "Escape") onClose(false);
    }
    if (open) {
      document.addEventListener("keydown", handleKey);
      return () => document.removeEventListener("keydown", handleKey);
    }
  }, [open, onClose]);

  // Body scroll lock
  useEffect(() => {
    if (open) {
      const original = document.body.style.overflow;
      document.body.style.overflow = "hidden";
      return () => {
        document.body.style.overflow = original;
      };
    }
  }, [open]);

  // Focus panel on open
  useEffect(() => {
    if (open && panelRef.current) {
      panelRef.current.focus();
    }
  }, [open]);

  if (!open) return null;

  const sizeClass = sizeMap[size] || sizeMap.md;

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
      {/* Backdrop */}
      <div
        className="fixed inset-0 bg-zinc-950/25 backdrop-blur-sm dark:bg-zinc-950/50"
        aria-hidden="true"
        onClick={() => onClose(false)}
      />
      {/* Panel */}
      <div
        ref={panelRef}
        tabIndex={-1}
        role="dialog"
        aria-modal="true"
        className={`relative w-full ${sizeClass} rounded-xl border border-zinc-200 bg-white p-6 shadow-lg outline-none dark:border-zinc-700 dark:bg-zinc-900 ${className}`}
      >
        {children}
      </div>
    </div>
  );
}

/* ── DialogTitle ────────────────────────────────────── */
export function DialogTitle({
  children,
  className = "",
}: {
  children: React.ReactNode;
  className?: string;
}) {
  return (
    <h3 className={`text-base font-semibold text-zinc-900 dark:text-zinc-100 ${className}`}>
      {children}
    </h3>
  );
}

/* ── DialogDescription ──────────────────────────────── */
export function DialogDescription({
  children,
  className = "",
}: {
  children: React.ReactNode;
  className?: string;
}) {
  return (
    <p className={`text-sm text-zinc-500 dark:text-zinc-400 ${className}`}>
      {children}
    </p>
  );
}

/* ── DialogBody ─────────────────────────────────────── */
export function DialogBody({
  children,
  className = "",
}: {
  children: React.ReactNode;
  className?: string;
}) {
  return <div className={className}>{children}</div>;
}

/* ── DialogActions ──────────────────────────────────── */
export function DialogActions({
  children,
  className = "",
}: {
  children: React.ReactNode;
  className?: string;
}) {
  return (
    <div className={`flex items-center justify-end gap-3 pt-4 ${className}`}>
      {children}
    </div>
  );
}