All components
Animated Dialog
modalsUi-Layouts component.
responsive · 640px
Install
Same command in any shadcn project — React (Vite/CRA), Next.js, Remix, Astro, and more:
$
npx shadcn@latest add https://your-domain/r/dialog.jsonUsage
"use client";
import { FramerModal, ModalContent } from "@/registry/ui-layouts/dialog";
import { useState } from "react";
export default function Demo() {
const [open, setOpen] = useState(false);
return (
<div className="flex h-full items-center justify-center">
<button
onClick={() => setOpen(true)}
className="px-6 py-3 rounded-lg border bg-background hover:bg-muted transition-colors font-medium text-sm"
>
Open Dialog
</button>
<FramerModal open={open} setOpen={setOpen}>
<ModalContent>
<h2 className="text-lg font-semibold text-foreground mb-1">
Confirm Action
</h2>
<p className="text-sm text-muted-foreground mb-6">
Are you sure you want to proceed? This action cannot be undone and
will permanently delete your data.
</p>
<div className="flex gap-3">
<button
onClick={() => setOpen(false)}
className="flex-1 py-2 rounded-md bg-foreground text-background text-sm font-medium hover:opacity-90 transition-opacity"
>
Confirm
</button>
<button
onClick={() => setOpen(false)}
className="flex-1 py-2 rounded-md border text-sm font-medium hover:bg-muted transition-colors"
>
Cancel
</button>
</div>
</ModalContent>
</FramerModal>
</div>
);
}Component source
'use client';
import { X } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import React, { createContext, type ReactNode, useContext, useEffect, useState } from 'react';
interface ModalContextProps {
open: boolean;
setOpen: (open: boolean) => void;
}
const ModalContext = createContext<ModalContextProps | undefined>(undefined);
const useModal = () => {
const context = useContext(ModalContext);
if (!context) {
throw new Error('useModal must be used within a ModalProvider');
}
return context;
};
interface FramerModalProps {
children: ReactNode;
open?: boolean;
setOpen?: (open: boolean) => void;
}
export function FramerModal({
children,
open: controlledOpen,
setOpen: controlledSetOpen,
}: FramerModalProps) {
const [internalOpen, setInternalOpen] = useState(false);
const open = controlledOpen !== undefined ? controlledOpen : internalOpen;
const setOpen = controlledSetOpen !== undefined ? controlledSetOpen : setInternalOpen;
useEffect(() => {
if (open) {
document.body.classList.add('overflow-hidden');
} else {
document.body.classList.remove('overflow-hidden');
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setOpen(false);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [open, setOpen]);
return (
<ModalContext.Provider value={{ open, setOpen }}>
<AnimatePresence>
{open && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className='fixed inset-0 z-50 top-0 left-0 right-0 bottom-0 flex flex-col items-center w-full h-screen justify-center dark:bg-black/90 bg-white/90 backdrop-blur-xs cursor-zoom-out border'
onClick={() => setOpen(false)}
>
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
onClick={(e) => e.stopPropagation()}
className=' w-full max-w-md rounded-xl bg-white/5 p-6 backdrop-blur-2xl border'
>
<button className='absolute top-2 right-2' onClick={() => setOpen(false)}>
<X />
</button>
{children}
</motion.div>
</motion.div>
)}
</AnimatePresence>
</ModalContext.Provider>
);
}
export function ModalContent({ children }: { children: ReactNode }) {
return <>{children}</>;
}Dependencies
motionlucide-react
Source: Ui-Layouts