React Easy Modals
uimodalsource: ok · hit
Modal (Radix)
Modal component for react-easy-modals with Radix Dialog.
npx shadcn add "https://react-easy-modals-docs.vercel.app/r/styles/{style}/modal.json"free
Interaction
modal
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightreact-easy-modals
Registry dependencies
dialog
Files1 · source cached 10/8/2026, 6:00:05 PM
src/registry/radix/modal.tsx
"use client";
import { createContext, useContext, useRef, useState } from "react";
import { useBeforeClose, useModal, useModals } from "react-easy-modals";
import { cn } from "@/lib/utils";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
type ModalContextValue = {
onAnimationEnd?: () => void;
};
const ModalContext = createContext<ModalContextValue>({});
const ModalClose = DialogClose;
const ModalDescription = DialogDescription;
const ModalFooter = DialogFooter;
const ModalHeader = DialogHeader;
const ModalOverlay = DialogOverlay;
const ModalPortal = DialogPortal;
const ModalTitle = DialogTitle;
const ModalTrigger = DialogTrigger;
type ModalContentProps = React.ComponentProps<typeof DialogContent>;
function ModalContent({
children,
className,
onAnimationEnd,
...props
}: ModalContentProps) {
const ctx = useContext(ModalContext);
return (
<DialogContent
className={cn(className)}
onAnimationEnd={(...e) => { onAnimationEnd?.(...e); ctx.onAnimationEnd?.() }}
{...props}
>
{children}
</DialogContent>
);
}
type ModalRootProps = {
children: React.ReactNode;
} & Omit<React.ComponentProps<typeof Dialog>, "open" | "onOpenChange">;
function Modal({ children, ...dialogProps }: ModalRootProps) {
const modal = useModal();
const modals = useModals();
const [isVisible, setIsVisible] = useState(true);
const canClose = useRef(false);
const closableVariable = useRef<Parameters<typeof modal.close>[0]>(undefined);
const currentModalIndex = modals.stack.findIndex((m) => m.id === modal.id);
const isTopModal = modal.isNested || currentModalIndex === modals.stack.length - 1;
const modalIsHidden = !modal.isNested && currentModalIndex !== -1 && !isTopModal;
useBeforeClose<Parameters<typeof modal.close>[0]>((closeValue) => {
closableVariable.current = closeValue;
setIsVisible(false);
return canClose.current;
});
function handleOpenChange(value: boolean) {
if (!value && isTopModal) {
setIsVisible(false);
}
}
function handleAnimationEnd() {
if (!isVisible) {
canClose.current = true;
modal.close(closableVariable.current);
closableVariable.current = undefined;
}
}
return (
<ModalContext.Provider value={{ onAnimationEnd: handleAnimationEnd }}>
<Dialog
open={isVisible && !modalIsHidden}
onOpenChange={handleOpenChange}
{...dialogProps}
>
{children}
</Dialog>
</ModalContext.Provider>
);
}
export {
Modal,
ModalClose,
ModalContent,
ModalDescription,
ModalFooter,
ModalHeader,
ModalOverlay,
ModalPortal,
ModalTitle,
ModalTrigger,
};