Drawer
Drawer is a React and Tailwind CSS overlay component from SmoothUI, licensed MIT. Copy it and paste it into your project.
SmoothUI
MIT
overlays
What it is
Drawer is a React and Tailwind CSS overlay component from SmoothUI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i motion
Licence
This component comes from SmoothUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Drawer
"use client";
import {
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
Drawer as DrawerPrimitive,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useCallback, useEffect, useRef, useState } from "react";
/* ------------------------------------------------------------------ */
/* Animation constants */
/* ------------------------------------------------------------------ */
const BACKDROP_DURATION = 0.2;
const STAGGER_BASE_DELAY = 0.12;
const STAGGER_CHILD_DELAY = 0.05;
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
export type DrawerSide = "top" | "right" | "bottom" | "left";
export interface DrawerProps {
/** Drawer content */
children?: React.ReactNode;
/** Additional CSS class names */
className?: string;
/** Description displayed below the title */
description?: string;
/** Footer content */
footer?: React.ReactNode;
/** Callback when the open state changes */
onOpenChange?: (open: boolean) => void;
/** Whether the drawer is open */
open?: boolean;
/** The side from which the drawer opens */
side?: DrawerSide;
/** Title displayed in the drawer header */
title?: string;
/** Trigger element that opens the drawer */
trigger?: React.ReactNode;
}
/* ------------------------------------------------------------------ */
/* Stagger child — animates content items in sequence after open */
/* ------------------------------------------------------------------ */
const StaggerChild = ({
children,
index,
shouldReduceMotion,
}: {
children: React.ReactNode;
index: number;
shouldReduceMotion: boolean | null;
}) => (
<motion.div
animate={
shouldReduceMotion
? { opacity: 1 }
: { opacity: 1, transform: "translateY(0px)" }
}
exit={
shouldReduceMotion
? { opacity: 0, transition: { duration: 0 } }
: { opacity: 0, transition: { duration: 0.1 } }
}
initial={
shouldReduceMotion
? { opacity: 1 }
: { opacity: 0, transform: "translateY(6px)" }
}
transition={
shouldReduceMotion
? { duration: 0 }
: {
bounce: 0,
delay: STAGGER_BASE_DELAY + index * STAGGER_CHILD_DELAY,
duration: 0.25,
type: "spring" as const,
}
}
>
{children}
</motion.div>
);
/* ------------------------------------------------------------------ */
/* Animated handle bar with glow pulse */
/* ------------------------------------------------------------------ */
const AnimatedHandle = ({
shouldReduceMotion,
}: {
shouldReduceMotion: boolean | null;
}) => (
<motion.div
animate={
shouldReduceMotion
? {}
: {
opacity: [0.5, 1, 0.5],
}
}
className="mx-auto mt-4 h-2 w-[100px] shrink-0 rounded-full bg-muted"
transition={
shouldReduceMotion
? { duration: 0 }
: {
duration: 2,
ease: [0.645, 0.045, 0.355, 1],
repeat: Number.POSITIVE_INFINITY,
}
}
/>
);
/* ------------------------------------------------------------------ */
/* Drawer */
/* ------------------------------------------------------------------ */
export default function Drawer({
open,
onOpenChange,
side = "bottom",
title,
description,
className,
children,
trigger,
footer,
}: DrawerProps) {
const shouldReduceMotion = useReducedMotion();
const [isAnimatingOut, setIsAnimatingOut] = useState(false);
const prevOpenRef = useRef(false);
useEffect(() => {
// When going from open -> closed, we need animation-out time
if (prevOpenRef.current && !open) {
setIsAnimatingOut(true);
}
prevOpenRef.current = !!open;
}, [open]);
const handleExitComplete = useCallback(() => {
setIsAnimatingOut(false);
}, []);
// Keep vaul's drawer open during exit animation so the portal stays mounted
const vaulOpen = open || isAnimatingOut;
return (
<DrawerPrimitive
direction={side}
onOpenChange={(next) => {
if (!next && isAnimatingOut) {
return; // Ignore vaul's close during our exit animation
}
onOpenChange?.(next);
}}
open={vaulOpen}
>
{trigger ? <DrawerTrigger asChild>{trigger}</DrawerTrigger> : null}
{/*
We render DrawerContent so vaul's drag gestures still work, but we
layer motion.div overlays on top for the visual polish (backdrop blur,
spring slide-in, content stagger, handle pulse).
*/}
<DrawerContent
className={cn(
// Hide vaul's default CSS transition — we animate with motion
"!transition-none !duration-0",
// Hide the default handle for bottom drawers — we render our own animated one
side === "bottom" &&
"[&>[class*='h-2'][class*='rounded-full']]:hidden",
className
)}
>
{/* Animated handle for bottom drawers */}
{side === "bottom" && (
<AnimatedHandle shouldReduceMotion={shouldReduceMotion} />
)}
<AnimatePresence onExitComplete={handleExitComplete}>
{open ? (
<motion.div
animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1 }}
exit={
shouldReduceMotion
? { opacity: 0, transition: { duration: 0 } }
: { opacity: 0, transition: { duration: 0.15 } }
}
initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}
transition={{
duration: shouldReduceMotion ? 0 : BACKDROP_DURATION,
}}
>
{/* Staggered header */}
{title || description ? (
<StaggerChild index={0} shouldReduceMotion={shouldReduceMotion}>
<DrawerHeader>
{title ? <DrawerTitle>{title}</DrawerTitle> : null}
{description ? (
<DrawerDescription>{description}</DrawerDescription>
) : null}
</DrawerHeader>
</StaggerChild>
) : null}
{/* Staggered body */}
{children ? (
<StaggerChild
index={title || description ? 1 : 0}
shouldReduceMotion={shouldReduceMotion}
>
<div className="px-4">{children}</div>
</StaggerChild>
) : null}
{/* Staggered footer */}
{footer ? (
<StaggerChild
index={(title || description ? 1 : 0) + (children ? 1 : 0)}
shouldReduceMotion={shouldReduceMotion}
>
<DrawerFooter>{footer}</DrawerFooter>
</StaggerChild>
) : null}
</motion.div>
) : null}
</AnimatePresence>
</DrawerContent>
</DrawerPrimitive>
);
}
/* ------------------------------------------------------------------ */
/* Re-exports */
/* ------------------------------------------------------------------ */
export {
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
};