Docs
Dialog
Dialog
Displays a dialog modal component.
Installation
Copy and paste the following code into your component.
"use client";
import React, {
createContext,
useContext,
useEffect,
useState,
useRef,
} from "react";
import ReactDOM from "react-dom";
import { Button, ButtonProps } from "@/components/ui/button/button";
import styles from "./dialog.module.css";
import { useRefs } from "@/components/lib/useRefs";
/* -------------------------------------------------------------------------------------------------
* DIALOG CONTEXT
* -----------------------------------------------------------------------------------------------*/
type DialogContextValue = {
open: boolean;
triggerRef: React.RefObject<HTMLButtonElement>;
contentRef: React.RefObject<HTMLDivElement>;
onOpenChange(open: boolean): void;
onOpenToggle(): void;
};
const DialogContext = createContext<DialogContextValue | undefined>(undefined);
const Dialog: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [open, setOpen] = useState(false);
const triggerRef = useRef<HTMLButtonElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const onOpenChange = (isOpen: boolean) => {
setOpen(isOpen);
};
const onOpenToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const contextValue: DialogContextValue = {
contentRef,
triggerRef,
open,
onOpenChange,
onOpenToggle,
};
return (
<DialogContext.Provider value={contextValue}>
{children}
</DialogContext.Provider>
);
};
const useDialog = () => {
const context = useContext(DialogContext);
if (!context) {
throw new Error("useDialog must be used within a DialogProvider");
}
return context;
};
/* -------------------------------------------------------------------------------------------------
* DIALOG TRIGGER
* -----------------------------------------------------------------------------------------------*/
const TRIGGER_NAME = "DialogTrigger";
type DialogTriggerElement = React.ElementRef<typeof Button>;
interface DialogTriggerProps extends ButtonProps {}
const DialogTrigger = React.forwardRef<
DialogTriggerElement,
DialogTriggerProps
>((props, ref) => {
const { ...triggerProps } = props;
const { open, onOpenToggle, triggerRef } = useDialog();
const combinedRefs = useRefs(ref, triggerRef);
return (
<Button
type='button'
aria-haspopup='dialog'
aria-expanded={open}
aria-controls='Dialog'
data-state={open}
{...triggerProps}
onClick={onOpenToggle}
ref={combinedRefs}
/>
);
});
DialogTrigger.displayName = TRIGGER_NAME;
const DialogClose: React.FC = () => {
const { onOpenToggle } = useDialog();
return (
<Button
className={styles.dialogClose}
variant='ghost'
size='small'
onClick={onOpenToggle}
aria-label='Close dialog'>
X
</Button>
);
};
/* -------------------------------------------------------------------------------------------------
* DIALOG PORTAL
* -----------------------------------------------------------------------------------------------*/
const PORTAL_NAME = "DialogPortal";
interface DialogPortalProps {
children?: React.ReactNode;
container?: Element;
}
const DialogPortal: React.FC<DialogPortalProps> = (props) => {
const { children, container } = props;
const c = container ?? globalThis?.document?.body;
const { open } = useDialog();
if (!open) {
return null;
}
return c
? ReactDOM.createPortal(
<div className={styles.dialogBase} {...props}>
{children}
</div>,
c
)
: null;
};
DialogPortal.displayName = PORTAL_NAME;
/* -------------------------------------------------------------------------------------------------
* DIALOG OVERLAY
* -----------------------------------------------------------------------------------------------*/
const OVERLAY_NAME = "DialogOverlay";
const DialogOverlay = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ ...props }, ref) => {
return <div {...props} className={styles.dialogOverlay} ref={ref} />;
});
DialogOverlay.displayName = OVERLAY_NAME;
/* -------------------------------------------------------------------------------------------------
* DIALOG CONTENT
* -----------------------------------------------------------------------------------------------*/
const CONTENT_NAME = "DialogContent";
interface DialogContentProps {
children?: React.ReactNode;
className?: String;
}
const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
({ children, className }, ref) => {
const { open, onOpenToggle, contentRef } = useDialog();
const combinedRefs = useRefs(contentRef, ref);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
onOpenToggle();
}
};
open && document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [open, onOpenToggle]);
useEffect(() => {
if (open) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
}, [open]);
return (
<DialogPortal>
<DialogOverlay />
<div
className={`${styles.dialogContent} ${className ?? ""}`}
role='dialog'
aria-labelledby='dialogTitle'
data-state={open ? "open" : "closed"}
ref={combinedRefs}>
{children}
<DialogClose />
</div>
</DialogPortal>
);
}
);
DialogContent.displayName = CONTENT_NAME;
/* -------------------------------------------------------------------------------------------------
* DIALOG HHEADER
* -----------------------------------------------------------------------------------------------*/
const HEADER_NAME = "DialogHeader";
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={`${styles.dialogHeader} ${className ?? ""}`} {...props} />
);
DialogHeader.displayName = HEADER_NAME;
/* -------------------------------------------------------------------------------------------------
* DIALOG TITLE
* -----------------------------------------------------------------------------------------------*/
const TITLE_NAME = "DialogTitle";
const DialogTitle = React.forwardRef<
HTMLHeadingElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h2
className={`${styles.dialogTitle} ${className ?? ""}`}
{...props}
ref={ref}
/>
));
DialogTitle.displayName = TITLE_NAME;
/* -------------------------------------------------------------------------------------------------
* DIALOG DESCRIPTION
* -----------------------------------------------------------------------------------------------*/
const DESCRIPTION_NAME = "DialogDescription";
const DialogDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
className={`${styles.dialogDescription} ${className ?? ""}`}
{...props}
ref={ref}
/>
));
DialogDescription.displayName = DESCRIPTION_NAME;
/* -------------------------------------------------------------------------------------------------
* DIALOG FOOTER
* -----------------------------------------------------------------------------------------------*/
const FOOTER_NAME = "DialogFooter";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={`${styles.dialogFooter} ${className ?? ""}`} {...props} />
);
DialogFooter.displayName = FOOTER_NAME;
export {
DialogPortal,
DialogOverlay,
DialogContent,
DialogClose,
DialogTrigger,
DialogTitle,
DialogHeader,
DialogDescription,
DialogFooter,
};
export { Dialog, useDialog };
Copy and paste the following CSS into a .module.css file.
.dialogBase {
bottom: 0;
position: fixed;
top: 0;
transition: 0.1s ease-in-out all;
width: 100%;
}
.dialogOverlay {
background-color: hsla(0, 0%, 0%, 0.5);
bottom: 0;
position: absolute;
top: 0;
width: 100%;
}
.dialogContent {
background: hsl(var(--background));
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
margin: 25vh auto;
max-width: 515px;
padding: 1.125rem;
position: relative;
}
.dialogClose {
position: absolute;
top: 0;
right: 0;
margin: 8px 5px;
}
.dialogHeader {
display: flex;
flex-direction: column;
text-align: center;
}
.dialogHeader > * {
margin-block-start: 0.25rem;
}
.dialogTitle {
font-weight: var(--semiBold);
font-size: var(--text-lg);
line-height: 1;
}
.dialogDescription {
color: hsl(var(--muted-foreground));
font-size: var(--text-sm);
line-height: 1.25rem;
}
.dialogFooter {
display: flex;
flex-direction: column-reverse;
}
@media (min-width: 640px) {
.dialogHeader {
text-align: left;
}
.dialogFooter {
flex-direction: row;
justify-content: flex-end;
}
.dialogFooter > * {
margin-inline: 0.5rem;
}
}
Update the import paths to match your project setup.
Usage
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/registry/ui/dialog/dialog";
<Dialog>
<DialogTrigger>Open</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you are done.
</DialogDescription>
</DialogHeader>
<span>Some awesome dialog stuff right here.</span>
<DialogFooter>
<Button variant='outline'>Cancel</Button>
<Button>Submit</Button>
</DialogFooter>
</DialogContent>
</Dialog>