Registry
The Modal component is available as a shadcn/ui registry component. This provides a pre-built Modal wrapper that integrates react-easy-modals with shadcn’s Dialog component.
Installation
npx shadcn@latest add @react-easy-modals/modalUsage
Simply replace Dialog with Modal in your imports:
- import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
+ import { Modal, ModalContent, ModalHeader, ModalTitle } from "@/components/ui/modal"No more DialogTrigger - just use modals.open() anywhere in your application:
- <Dialog>
- <DialogTrigger asChild>
- <Button>Open</Button>
- </DialogTrigger>
- <DialogContent>...</DialogContent>
- </Dialog>
+ const modals = useModals()
+ <Button onClick={() => modals.open(MyModal)}>Open</Button>Example
Here’s a simple confirm modal using the registry component:
import { ModalProps, ModalProvider, useModals } from "react-easy-modals";
import {
Modal,
ModalContent,
ModalDescription,
ModalFooter,
ModalHeader,
ModalTitle,
} from "@/components/ui/modal";
import { Button } from "@/components/ui/button";
type ConfirmModalReturnType = "confirm" | "cancel";
function ConfirmModal({ close }: ModalProps<ConfirmModalReturnType>) {
return (
<Modal>
<ModalContent>
<ModalHeader>
<ModalTitle>Confirm Action</ModalTitle>
<ModalDescription>
"Are you sure you want to proceed?
</ModalDescription>
</ModalHeader>
<ModalFooter>
<Button variant="outline" onClick={() => close("cancel")}>
Cancel
</Button>
<Button onClick={() => close("confirm")}>Confirm</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
function App() {
const modals = useModals();
const handleClick = async () => {
const result = await modals.open(ConfirmModal);
if (result === "confirm") {
// User confirmed
}
};
return <Button onClick={handleClick}>Open Modal</Button>;
}Nested Modals
For nested modals , the react-easy-modals nested API works perfectly with Base UI’s nested dialogs .
To use nested modals, you must install shadcn with base-ui component library.
import { ModalProps, ModalProvider, useModalAt, useModals } from "react-easy-modals";
import {
Modal,
ModalContent,
ModalDescription,
ModalFooter,
ModalHeader,
ModalTitle,
} from "@/components/ui/modal";
import { Button } from "@/components/ui/button";
function ChildModal({ close, open, title, nested }: ModalProps & { title: string }) {
const modals = useModals();
const root = useModalAt("root");
const parent = useModalAt(-1);
return (
<Modal>
<ModalContent>
{nested}
<ModalHeader>
<ModalTitle>{title}</ModalTitle>
<ModalDescription>
Open another nested modal, close this one, or close the root.
</ModalDescription>
</ModalHeader>
<ModalFooter className="flex flex-wrap gap-2">
<Button
variant="outline"
onClick={() => open(ChildModal, { title: `Nested under ${title}` })}
>
Open nested
</Button>
<Button variant="outline" onClick={() => close()}>
Close this
</Button>
<Button variant="outline" onClick={() => parent.close()}>
Close parent
</Button>
<Button variant="outline" onClick={() => modals.open(ParentModal)}>
Open new root
</Button>
<Button onClick={() => root.close()}>Close root</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
function ParentModal({ open, close, nested }: ModalProps) {
return (
<Modal>
<ModalContent>
{nested}
<ModalHeader>
<ModalTitle>Parent Modal</ModalTitle>
<ModalDescription>Open a nested modal inside this one.</ModalDescription>
</ModalHeader>
<ModalFooter>
<Button variant="outline" onClick={() => close()}>
Close
</Button>
<Button onClick={() => open(ChildModal, { title: "Nested!" })}>
Open nested
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
function App() {
const modals = useModals();
return <Button onClick={() => modals.open(ParentModal)}>Open</Button>;
}
export default function NestedExample() {
return (
<ModalProvider>
<App />
</ModalProvider>
);
}Nested dialog style
To enable the shadcn Base UI nested dialog style, make sure your
DialogContent adds the nested transform and shadow classes inside the
DialogPrimitive.Popup className={cn(...)} block.
<DialogPrimitive.Popup
data-slot="dialog-content"
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
+ "translate-y-[calc(1.20rem*var(--nested-dialogs,0))] scale-[calc(1-0.08*var(--nested-dialogs,0))] transition-[scale,translate]",
+ "data-nested:shadow-xl",
className
)}
/>