Dialog
Dialog is a React and Tailwind CSS overlay component from Motion Primitives, licensed MIT. Copy it and paste it into your project.
Motion Primitives
MIT
overlays
What it is
Dialog is a React and Tailwind CSS overlay component from Motion Primitives, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i lucide-react motion
Licence
This component comes from Motion Primitives and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Dialog
'use client';
import { AnimatePresence, motion, Transition, Variants } from 'motion/react';
import React, { createContext, useContext, useEffect, useRef } from 'react';
import { cn } from '@/lib/utils';
import { useId } from 'react';
import { createPortal } from 'react-dom';
import { X } from 'lucide-react';
import { usePreventScroll } from '@/hooks/usePreventScroll';
const DialogContext = createContext<{
isOpen: boolean;
setIsOpen: (open: boolean) => void;
dialogRef: React.RefObject<HTMLDialogElement | null>;
variants: Variants;
transition?: Transition;
ids: {
dialog: string;
title: string;
description: string;
};
onAnimationComplete: (definition: string) => void;
handleTrigger: () => void;
} | null>(null);
const defaultVariants: Variants = {
initial: {
opacity: 0,
scale: 0.9,
},
animate: {
opacity: 1,
scale: 1,
},
};
const defaultTransition: Transition = {
ease: 'easeOut',
duration: 0.2,
};
export type DialogProps = {
children: React.ReactNode;
variants?: Variants;
transition?: Transition;
className?: string;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
open?: boolean;
};
function Dialog({
children,
variants = defaultVariants,
transition = defaultTransition,
defaultOpen,
onOpenChange,
open,
}: DialogProps) {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(
defaultOpen || false
);
const dialogRef = useRef<HTMLDialogElement>(null);
const isOpen = open !== undefined ? open : uncontrolledOpen;
// prevent scroll when dialog is open on iOS
usePreventScroll({
isDisabled: !isOpen,
});
const setIsOpen = React.useCallback(
(value: boolean) => {
setUncontrolledOpen(value);
onOpenChange?.(value);
},
[onOpenChange]
);
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (isOpen) {
document.body.classList.add('overflow-hidden');
} else {
document.body.classList.remove('overflow-hidden');
}
const handleCancel = (e: Event) => {
e.preventDefault();
if (isOpen) {
setIsOpen(false);
}
};
dialog.addEventListener('cancel', handleCancel);
return () => {
dialog.removeEventListener('cancel', handleCancel);
document.body.classList.remove('overflow-hidden');
};
}, [dialogRef, isOpen, setIsOpen]);
useEffect(() => {
if (isOpen && dialogRef.current) {
dialogRef.current.showModal();
}
}, [isOpen]);
const handleTrigger = () => {
setIsOpen(true);
};
const onAnimationComplete = (definition: string) => {
if (definition === 'exit' && !isOpen) {
dialogRef.current?.close();
}
};
const baseId = useId();
const ids = {
dialog: `motion-ui-dialog-${baseId}`,
title: `motion-ui-dialog-title-${baseId}`,
description: `motion-ui-dialog-description-${baseId}`,
};
return (
<DialogContext.Provider
value={{
isOpen,
setIsOpen,
dialogRef,
variants,
transition,
ids,
onAnimationComplete,
handleTrigger,
}}
>
{children}
</DialogContext.Provider>
);
}
export type DialogTriggerProps = {
children: React.ReactNode;
className?: string;
};
function DialogTrigger({ children, className }: DialogTriggerProps) {
const context = useContext(DialogContext);
if (!context) throw new Error('DialogTrigger must be used within Dialog');
return (
<button
onClick={context.handleTrigger}
className={cn(
'inline-flex items-center justify-center rounded-md text-sm font-medium',
'transition-colors focus-visible:ring-2 focus-visible:outline-hidden',
'focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
className
)}
>
{children}
</button>
);
}
export type DialogPortalProps = {
children: React.ReactNode;
container?: HTMLElement | null;
};
function DialogPortal({
children,
container = typeof window !== 'undefined' ? document.body : null,
}: DialogPortalProps) {
const [mounted, setMounted] = React.useState(false);
const [portalContainer, setPortalContainer] =
React.useState<HTMLElement | null>(null);
useEffect(() => {
setMounted(true);
setPortalContainer(container || document.body);
return () => setMounted(false);
}, [container]);
if (!mounted || !portalContainer) {
return null;
}
return createPortal(children, portalContainer);
}
export type DialogContentProps = {
children: React.ReactNode;
className?: string;
container?: HTMLElement;
};
function DialogContent({ children, className, container }: DialogContentProps) {
const context = useContext(DialogContext);
if (!context) throw new Error('DialogContent must be used within Dialog');
const {
isOpen,
setIsOpen,
dialogRef,
variants,
transition,
ids,
onAnimationComplete,
} = context;
const content = (
<AnimatePresence mode='wait'>
{isOpen && (
<motion.dialog
key={ids.dialog}
ref={dialogRef as React.RefObject<HTMLDialogElement>}
id={ids.dialog}
aria-labelledby={ids.title}
aria-describedby={ids.description}
aria-modal='true'
role='dialog'
onClick={(e) => {
if (e.target === dialogRef.current) {
setIsOpen(false);
}
}}
initial='initial'
animate='animate'
exit='exit'
variants={variants}
transition={transition}
onAnimationComplete={onAnimationComplete}
className={cn(
'fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transform rounded-lg border border-zinc-200 p-0 shadow-lg dark:border dark:border-zinc-700',
'backdrop:bg-black/50 backdrop:backdrop-blur-xs',
'open:flex open:flex-col',
className
)}
>
<div className='w-full'>{children}</div>
</motion.dialog>
)}
</AnimatePresence>
);
return <DialogPortal container={container}>{content}</DialogPortal>;
}
export type DialogHeaderProps = {
children: React.ReactNode;
className?: string;
};
function DialogHeader({ children, className }: DialogHeaderProps) {
return (
<div className={cn('flex flex-col space-y-1.5', className)}>{children}</div>
);
}
export type DialogTitleProps = {
children: React.ReactNode;
className?: string;
};
function DialogTitle({ children, className }: DialogTitleProps) {
const context = useContext(DialogContext);
if (!context) throw new Error('DialogTitle must be used within Dialog');
return (
<h2
id={context.ids.title}
className={cn('text-base font-medium', className)}
>
{children}
</h2>
);
}
export type DialogDescriptionProps = {
children: React.ReactNode;
className?: string;
};
function DialogDescription({ children, className }: DialogDescriptionProps) {
const context = useContext(DialogContext);
if (!context) throw new Error('DialogDescription must be used within Dialog');
return (
<p
id={context.ids.description}
className={cn('text-base text-zinc-500', className)}
>
{children}
</p>
);
}
export type DialogCloseProps = {
className?: string;
children?: React.ReactNode;
disabled?: boolean;
};
function DialogClose({ className, children, disabled }: DialogCloseProps) {
const context = useContext(DialogContext);
if (!context) throw new Error('DialogClose must be used within Dialog');
return (
<button
onClick={() => context.setIsOpen(false)}
type='button'
aria-label='Close dialog'
className={cn(
'absolute top-4 right-4 rounded-xs opacity-70 transition-opacity',
'hover:opacity-100 focus:ring-2 focus:outline-hidden',
'focus:ring-zinc-500 focus:ring-offset-2 disabled:pointer-events-none',
className
)}
disabled={disabled}
>
{children || <X className='h-4 w-4' />}
<span className='sr-only'>Close</span>
</button>
);
}
export {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogClose,
};