Dialog
Dialog é um componente de sobreposição para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.
SmoothUI
MIT
overlays
O que é
Dialog é um componente de sobreposição para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Precisa de
npm i lucide-react motion radix-ui
Licença
Este componente vem de SmoothUI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.
Dialog
"use client";
import {
AlertDialogAction,
AlertDialogCancel,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import {
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import { X } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import {
AlertDialog as AlertDialogRadix,
Dialog as DialogRadix,
} from "radix-ui";
import { useCallback, useEffect, useRef, useState } from "react";
/* ------------------------------------------------------------------ */
/* Shared animation constants */
/* ------------------------------------------------------------------ */
const SPRING_PANEL = {
bounce: 0.05,
duration: 0.25,
type: "spring" as const,
};
const BACKDROP_DURATION = 0.2;
const STAGGER_DELAY = 0.06;
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
export interface DialogProps {
/** Dialog content */
children?: React.ReactNode;
/** Additional CSS class names for the content */
className?: string;
/** Description displayed below the title */
description?: string;
/** Footer content */
footer?: React.ReactNode;
/** Callback when the open state changes */
onOpenChange?: (open: boolean) => void;
/** Whether the dialog is open */
open?: boolean;
/** Whether to show the close button */
showCloseButton?: boolean;
/** Title displayed in the dialog header */
title?: string;
/** Trigger element that opens the dialog */
trigger?: React.ReactNode;
}
export interface AlertDialogProps {
/** Alert dialog content */
children?: React.ReactNode;
/** Additional CSS class names for the content */
className?: string;
/** Description displayed below the title */
description?: string;
/** Footer content (typically AlertDialogAction + AlertDialogCancel) */
footer?: React.ReactNode;
/** Callback when the open state changes */
onOpenChange?: (open: boolean) => void;
/** Whether the alert dialog is open */
open?: boolean;
/** Title displayed in the alert dialog header */
title?: string;
/** Trigger element that opens the alert dialog */
trigger?: React.ReactNode;
}
/* ------------------------------------------------------------------ */
/* Stagger wrapper — animates children in sequence */
/* ------------------------------------------------------------------ */
const StaggerChild = ({
children,
index,
shouldReduceMotion,
}: {
children: React.ReactNode;
index: number;
shouldReduceMotion: boolean | null;
}) => (
<motion.div
animate={
shouldReduceMotion
? { opacity: 1 }
: { opacity: 1, transform: "translateY(0px)" }
}
exit={
shouldReduceMotion
? { opacity: 0, transition: { duration: 0 } }
: {
opacity: 0,
transform: "translateY(4px)",
transition: { duration: 0.12 },
}
}
initial={
shouldReduceMotion
? { opacity: 1 }
: { opacity: 0, transform: "translateY(8px)" }
}
transition={
shouldReduceMotion
? { duration: 0 }
: {
bounce: 0,
delay: index * STAGGER_DELAY,
duration: 0.25,
type: "spring" as const,
}
}
>
{children}
</motion.div>
);
/* ------------------------------------------------------------------ */
/* Animated close button with rotate on hover */
/* ------------------------------------------------------------------ */
const AnimatedCloseButton = ({
onClick,
shouldReduceMotion,
}: {
onClick: () => void;
shouldReduceMotion: boolean | null;
}) => (
<motion.button
aria-label="Close dialog"
className="absolute top-4 right-4 cursor-pointer rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none"
onClick={onClick}
transition={{ duration: shouldReduceMotion ? 0 : 0.2 }}
type="button"
whileHover={shouldReduceMotion ? {} : { rotate: 90 }}
>
<X aria-hidden="true" className="pointer-events-none size-4 shrink-0" />
<span className="sr-only">Close</span>
</motion.button>
);
/* ------------------------------------------------------------------ */
/* Dialog */
/* ------------------------------------------------------------------ */
export default function Dialog({
open,
onOpenChange,
title,
description,
showCloseButton = true,
className,
children,
trigger,
footer,
}: DialogProps) {
const shouldReduceMotion = useReducedMotion();
const [internalOpen, setInternalOpen] = useState(false);
const isControlled = open !== undefined;
const isOpen = isControlled ? open : internalOpen;
const handleOpenChange = useCallback(
(next: boolean) => {
if (!isControlled) {
setInternalOpen(next);
}
onOpenChange?.(next);
},
[isControlled, onOpenChange]
);
// Track mount state for AnimatePresence exit animations
const [showContent, setShowContent] = useState(false);
const prevOpenRef = useRef(false);
useEffect(() => {
if (isOpen && !prevOpenRef.current) {
setShowContent(true);
}
prevOpenRef.current = !!isOpen;
}, [isOpen]);
const handleAnimationComplete = useCallback(() => {
if (!isOpen) {
setShowContent(false);
}
}, [isOpen]);
return (
<DialogRadix.Root
onOpenChange={handleOpenChange}
open={isOpen || showContent}
>
{trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}
<AnimatePresence onExitComplete={() => setShowContent(false)}>
{isOpen ? (
<DialogRadix.Portal forceMount>
{/* Backdrop with blur + opacity fade */}
<DialogRadix.Overlay asChild forceMount>
<motion.div
animate={{ opacity: 1 }}
className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
data-slot="dialog-overlay"
exit={{ opacity: 0 }}
initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}
transition={{
duration: shouldReduceMotion ? 0 : BACKDROP_DURATION,
}}
/>
</DialogRadix.Overlay>
{/* Content panel — spring scale + translateY entrance */}
<DialogRadix.Content asChild forceMount>
<motion.div
animate={
shouldReduceMotion
? { opacity: 1 }
: {
opacity: 1,
transform: "translate(-50%, -50%) scale(1)",
}
}
className={cn(
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] gap-4 rounded-lg border bg-background p-6 shadow-lg sm:max-w-lg",
className
)}
data-slot="dialog-content"
exit={
shouldReduceMotion
? { opacity: 0, transition: { duration: 0 } }
: {
opacity: 0,
transform: "translate(-50%, -50%) scale(0.95)",
transition: { duration: 0.15 },
}
}
initial={
shouldReduceMotion
? {
opacity: 1,
transform: "translate(-50%, -50%) scale(1)",
}
: {
opacity: 0,
transform: "translate(-50%, -48%) scale(0.95)",
}
}
onAnimationComplete={handleAnimationComplete}
transition={shouldReduceMotion ? { duration: 0 } : SPRING_PANEL}
>
{/* Staggered content sections */}
{title || description ? (
<StaggerChild
index={0}
shouldReduceMotion={shouldReduceMotion}
>
<DialogHeader>
{title ? <DialogTitle>{title}</DialogTitle> : null}
{description ? (
<DialogDescription>{description}</DialogDescription>
) : null}
</DialogHeader>
</StaggerChild>
) : null}
{children ? (
<StaggerChild
index={title || description ? 1 : 0}
shouldReduceMotion={shouldReduceMotion}
>
{children}
</StaggerChild>
) : null}
{footer ? (
<StaggerChild
index={(title || description ? 1 : 0) + (children ? 1 : 0)}
shouldReduceMotion={shouldReduceMotion}
>
<DialogFooter>{footer}</DialogFooter>
</StaggerChild>
) : null}
{showCloseButton ? (
<AnimatedCloseButton
onClick={() => handleOpenChange(false)}
shouldReduceMotion={shouldReduceMotion}
/>
) : null}
</motion.div>
</DialogRadix.Content>
</DialogRadix.Portal>
) : null}
</AnimatePresence>
</DialogRadix.Root>
);
}
/* ------------------------------------------------------------------ */
/* AlertDialog */
/* ------------------------------------------------------------------ */
export function AlertDialog({
open,
onOpenChange,
title,
description,
className,
children,
trigger,
footer,
}: AlertDialogProps) {
const shouldReduceMotion = useReducedMotion();
const [internalOpen, setInternalOpen] = useState(false);
const isControlled = open !== undefined;
const isOpen = isControlled ? open : internalOpen;
const handleOpenChange = useCallback(
(next: boolean) => {
if (!isControlled) {
setInternalOpen(next);
}
onOpenChange?.(next);
},
[isControlled, onOpenChange]
);
const [showContent, setShowContent] = useState(false);
const prevOpenRef = useRef(false);
useEffect(() => {
if (isOpen && !prevOpenRef.current) {
setShowContent(true);
}
prevOpenRef.current = !!isOpen;
}, [isOpen]);
const handleAnimationComplete = useCallback(() => {
if (!isOpen) {
setShowContent(false);
}
}, [isOpen]);
return (
<AlertDialogRadix.Root
onOpenChange={handleOpenChange}
open={isOpen || showContent}
>
{trigger ? (
<AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>
) : null}
<AnimatePresence onExitComplete={() => setShowContent(false)}>
{isOpen ? (
<AlertDialogRadix.Portal forceMount>
{/* Backdrop */}
<AlertDialogRadix.Overlay asChild forceMount>
<motion.div
animate={{ opacity: 1 }}
className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
data-slot="alert-dialog-overlay"
exit={{ opacity: 0 }}
initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}
transition={{
duration: shouldReduceMotion ? 0 : BACKDROP_DURATION,
}}
/>
</AlertDialogRadix.Overlay>
{/* Content panel */}
<AlertDialogRadix.Content asChild forceMount>
<motion.div
animate={
shouldReduceMotion
? { opacity: 1 }
: {
opacity: 1,
transform: "translate(-50%, -50%) scale(1)",
}
}
className={cn(
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] gap-4 rounded-lg border bg-background p-6 shadow-lg sm:max-w-lg",
className
)}
data-slot="alert-dialog-content"
exit={
shouldReduceMotion
? { opacity: 0, transition: { duration: 0 } }
: {
opacity: 0,
transform: "translate(-50%, -50%) scale(0.95)",
transition: { duration: 0.15 },
}
}
initial={
shouldReduceMotion
? {
opacity: 1,
transform: "translate(-50%, -50%) scale(1)",
}
: {
opacity: 0,
transform: "translate(-50%, -48%) scale(0.95)",
}
}
onAnimationComplete={handleAnimationComplete}
transition={shouldReduceMotion ? { duration: 0 } : SPRING_PANEL}
>
{title || description ? (
<StaggerChild
index={0}
shouldReduceMotion={shouldReduceMotion}
>
<AlertDialogHeader>
{title ? (
<AlertDialogTitle>{title}</AlertDialogTitle>
) : null}
{description ? (
<AlertDialogDescription>
{description}
</AlertDialogDescription>
) : null}
</AlertDialogHeader>
</StaggerChild>
) : null}
{children ? (
<StaggerChild
index={title || description ? 1 : 0}
shouldReduceMotion={shouldReduceMotion}
>
{children}
</StaggerChild>
) : null}
{footer ? (
<StaggerChild
index={(title || description ? 1 : 0) + (children ? 1 : 0)}
shouldReduceMotion={shouldReduceMotion}
>
<AlertDialogFooter>{footer}</AlertDialogFooter>
</StaggerChild>
) : null}
</motion.div>
</AlertDialogRadix.Content>
</AlertDialogRadix.Portal>
) : null}
</AnimatePresence>
</AlertDialogRadix.Root>
);
}
/* ------------------------------------------------------------------ */
/* Re-exports */
/* ------------------------------------------------------------------ */
export {
AlertDialogAction,
AlertDialogCancel,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
};