soralabs
uibase-dialogsource: ok · hit
Base Dialog
An accessible modal dialog built on Base UI with smooth Motion spring entrance and exit animations.
npx shadcn add "https://ui.soralabs.studio/r/base-dialog.json"free
Interaction
modal
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumui-primitives@base-ui/react
animationmotion
iconslucide-react
Registry dependencies
utils@soralabs/hooks-use-controlled-state@soralabs/base-button
Files1 · source cached 10/14/2026, 12:13:11 PM
registry/ui/base/dialog/index.tsx
"use client";
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
import { useControlledState } from "@/hooks/use-controlled-state";
import { cn } from "@/lib/utils";
import { XIcon } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { type ComponentProps, createContext, useContext, useMemo } from "react";
import { Button } from "@/components/sora-ui/base/button";
interface DialogContextValue {
open: boolean;
setOpen: (open: boolean) => void;
}
const DialogContext = createContext<DialogContextValue | null>(null);
function useDialogContext() {
return useContext(DialogContext);
}
type DialogProps = ComponentProps<typeof DialogPrimitive.Root>;
/**
* Root container for the Dialog component powered by Base UI and Motion.
*/
function Dialog({
open: controlledOpen,
defaultOpen = false,
onOpenChange,
children,
...props
}: DialogProps) {
const [open, setOpen] = useControlledState({
defaultValue: defaultOpen,
onChange: (nextOpen: boolean) => {
onOpenChange?.(
nextOpen,
undefined as unknown as DialogPrimitive.Root.ChangeEventDetails
);
},
value: controlledOpen,
});
const contextValue = useMemo(
() => ({ open: Boolean(open), setOpen }),
[open, setOpen]
);
return (
<DialogContext.Provider value={contextValue}>
<DialogPrimitive.Root
onOpenChange={(nextOpen, eventDetails) => {
setOpen(nextOpen);
onOpenChange?.(nextOpen, eventDetails);
}}
open={open}
{...props}
>
{children}
</DialogPrimitive.Root>
</DialogContext.Provider>
);
}
type DialogTriggerProps = ComponentProps<typeof DialogPrimitive.Trigger>;
/**
* Button or trigger element that opens the dialog.
* Features Motion hover and tap scaling.
*/
function DialogTrigger({ className, render, ...props }: DialogTriggerProps) {
const prefersReducedMotion = useReducedMotion();
return (
<DialogPrimitive.Trigger
className={className}
data-slot="dialog-trigger"
render={
render ?? (
<motion.button
data-primary-action
type="button"
whileHover={prefersReducedMotion ? undefined : { scale: 1.05 }}
whileTap={prefersReducedMotion ? undefined : { scale: 0.95 }}
/>
)
}
{...props}
/>
);
}
type DialogPortalProps = ComponentProps<typeof DialogPrimitive.Portal>;
/**
* Portals dialog overlay and content to the document body.
*/
function DialogPortal({ ...props }: DialogPortalProps) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
type DialogCloseProps = ComponentProps<typeof DialogPrimitive.Close>;
/**
* Action button that dismisses the dialog.
*/
function DialogClose({ className, render, ...props }: DialogCloseProps) {
const prefersReducedMotion = useReducedMotion();
return (
<DialogPrimitive.Close
className={className}
data-slot="dialog-close"
render={
render ?? (
<motion.button
type="button"
whileHover={prefersReducedMotion ? undefined : { scale: 1.05 }}
whileTap={prefersReducedMotion ? undefined : { scale: 0.95 }}
/>
)
}
{...props}
/>
);
}
type DialogOverlayProps = ComponentProps<typeof DialogPrimitive.Backdrop>;
/**
* Semi-transparent backdrop overlay behind the dialog with blur and fade transition.
*/
function DialogOverlay({ className, render, ...props }: DialogOverlayProps) {
const prefersReducedMotion = useReducedMotion();
return (
<DialogPrimitive.Backdrop
className={cn(
"fixed inset-0 isolate z-50 bg-black/50 backdrop-blur-xs dark:bg-black/70",
className
)}
data-slot="dialog-overlay"
render={
render ?? (
<motion.div
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
initial={{ opacity: 0 }}
transition={
prefersReducedMotion
? { duration: 0 }
: { duration: 0.2, ease: "easeOut" }
}
/>
)
}
{...props}
/>
);
}
type DialogViewportProps = ComponentProps<typeof DialogPrimitive.Viewport>;
/**
* Optional scroll viewport wrapper for large or overflowing dialog popups.
*/
function DialogViewport({ className, ...props }: DialogViewportProps) {
return (
<DialogPrimitive.Viewport
className={cn(
"fixed inset-0 z-50 flex items-center justify-center overflow-y-auto p-4",
className
)}
data-slot="dialog-viewport"
{...props}
/>
);
}
interface DialogContentProps
extends ComponentProps<typeof DialogPrimitive.Popup> {
/**
* Additional CSS class name for the fixed container wrapper.
*/
containerClassName?: string;
/**
* Additional CSS class name for the backdrop overlay.
*/
overlayClassName?: string;
/**
* Whether to render the standard top-right close icon button.
* @default true
*/
showCloseButton?: boolean;
}
/**
* Modal dialog content container with Motion 3D perspective rotation and blur entrance/exit.
*/
function DialogContent({
className,
children,
showCloseButton = true,
overlayClassName,
containerClassName,
render,
style,
...props
}: DialogContentProps) {
const context = useDialogContext();
const prefersReducedMotion = useReducedMotion();
const isOpen = context ? context.open : true;
return (
<AnimatePresence>
{isOpen && (
<DialogPrimitive.Portal keepMounted>
<DialogOverlay className={overlayClassName} />
<div
className={cn(
"pointer-events-none fixed inset-0 z-50 flex items-center justify-center p-4",
containerClassName
)}
>
<DialogPrimitive.Popup
className={cn(
"pointer-events-auto relative grid w-full max-w-[calc(100%-2rem)] gap-4 rounded-xl border border-border bg-background p-6 text-foreground text-sm shadow-2xl outline-none sm:max-w-lg",
className
)}
data-slot="dialog-content"
render={
render ?? (
<motion.div
animate={
prefersReducedMotion
? { opacity: 1 }
: {
opacity: 1,
filter: "blur(0px)",
rotateX: 0,
rotateY: 0,
z: 0,
transition: {
delay: 0.2,
duration: 0.5,
ease: [0.17, 0.67, 0.51, 1],
opacity: {
delay: 0.2,
duration: 0.5,
ease: "easeOut",
},
},
}
}
exit={
prefersReducedMotion
? { opacity: 0 }
: {
opacity: 0,
filter: "blur(10px)",
z: -100,
rotateY: 25,
rotateX: 5,
transformPerspective: 500,
transition: {
duration: 0.3,
ease: [0.67, 0.17, 0.62, 0.64],
},
}
}
initial={
prefersReducedMotion
? { opacity: 0 }
: {
opacity: 0,
filter: "blur(10px)",
z: -100,
rotateY: 25,
rotateX: 5,
transformPerspective: 500,
}
}
style={{
transformPerspective: 500,
transformStyle: "preserve-3d",
...style,
}}
/>
)
}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
aria-label="Close"
className="absolute top-4 right-4 inline-flex size-7 items-center justify-center rounded-md text-muted-foreground opacity-70 transition-opacity hover:bg-muted hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none"
data-slot="dialog-close-button"
render={
<motion.button
type="button"
whileHover={
prefersReducedMotion ? undefined : { scale: 1.1 }
}
whileTap={
prefersReducedMotion ? undefined : { scale: 0.95 }
}
/>
}
>
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Popup>
</div>
</DialogPrimitive.Portal>
)}
</AnimatePresence>
);
}
/**
* Header section containing dialog title and description.
*/
function DialogHeader({ className, ...props }: ComponentProps<"div">) {
return (
<div
className={cn(
"flex flex-col gap-1.5 text-center sm:text-left",
className
)}
data-slot="dialog-header"
{...props}
/>
);
}
interface DialogFooterProps extends ComponentProps<"div"> {
/**
* Whether to render a secondary close button inside the footer.
* @default false
*/
showCloseButton?: boolean;
}
/**
* Footer section for dialog action buttons.
*/
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: DialogFooterProps) {
return (
<div
className={cn(
"-mx-6 mt-2 -mb-6 flex flex-col-reverse gap-2 rounded-b-xl border-border border-t bg-muted/40 p-4 sm:flex-row sm:justify-end",
className
)}
data-slot="dialog-footer"
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-footer-close"
render={<Button variant="outline">Close</Button>}
/>
)}
</div>
);
}
type DialogTitleProps = ComponentProps<typeof DialogPrimitive.Title>;
/**
* Accessible title heading for the dialog.
*/
function DialogTitle({ className, ...props }: DialogTitleProps) {
return (
<DialogPrimitive.Title
className={cn(
"font-semibold text-foreground text-lg leading-none tracking-tight",
className
)}
data-slot="dialog-title"
{...props}
/>
);
}
type DialogDescriptionProps = ComponentProps<
typeof DialogPrimitive.Description
>;
/**
* Accessible description explaining the purpose of the dialog.
*/
function DialogDescription({ className, ...props }: DialogDescriptionProps) {
return (
<DialogPrimitive.Description
className={cn("text-muted-foreground text-sm", className)}
data-slot="dialog-description"
{...props}
/>
);
}
export type {
DialogCloseProps,
DialogContentProps,
DialogDescriptionProps,
DialogFooterProps,
DialogOverlayProps,
DialogPortalProps,
DialogProps,
DialogTitleProps,
DialogTriggerProps,
DialogViewportProps,
};
export {
Dialog,
Dialog as BaseDialog,
Dialog as default,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
DialogViewport,
};