Installation
Install the following dependencies:
npm install framer-motion lucide-react
Copy and paste the following code into your project.
components/ui/canvas-drawer.tsx
"use client";
import * as React from "react";
import { motion, AnimatePresence } from "framer-motion";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
export interface CanvasDrawerProps {
children: React.ReactNode;
open?: boolean;
onOpenChange?: (open: boolean) => void;
side?: "left" | "right";
className?: string;
showHandle?: boolean;
}
export function CanvasDrawer({
children,
open = false,
onOpenChange,
side = "right",
className,
showHandle = true,
}: CanvasDrawerProps) {
const [isOpen, setIsOpen] = React.useState(open);
React.useEffect(() => {
setIsOpen(open);
}, [open]);
const handleToggle = () => {
const newState = !isOpen;
setIsOpen(newState);
onOpenChange?.(newState);
};
return (
<>
{showHandle && (
<motion.button
onClick={handleToggle}
className={cn(
"group fixed top-1/2 z-50 -translate-y-1/2",
side === "left" ? "left-0" : "right-0",
)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
>
<div
className={cn(
"relative flex h-24 w-8 items-center justify-center",
"rounded-full bg-linear-to-b from-primary/20 to-primary/10",
"border border-primary/20 backdrop-blur-xs",
"dark:from-primary/30 dark:to-primary/20",
"transition-colors duration-200",
"group-hover:from-primary/30 group-hover:to-primary/20",
"dark:group-hover:from-primary/40 dark:group-hover:to-primary/30",
)}
>
<motion.div
className="absolute inset-0 rounded-full bg-primary/5"
initial={false}
animate={{
scale: [1, 1.2, 1],
opacity: [0.3, 0.2, 0.3],
}}
transition={{
duration: 3,
repeat: Infinity,
repeatType: "reverse",
}}
/>
<div
className={cn(
"h-1 w-4 rounded-full bg-primary/50",
"transition-transform duration-200",
isOpen ? "rotate-180" : "rotate-0",
side === "left" ? "-rotate-90" : "rotate-90",
)}
/>
</div>
</motion.button>
)}
<AnimatePresence>
{isOpen && (
<>
<motion.div
className="fixed inset-0 z-40 bg-black/20 backdrop-blur-xs"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={handleToggle}
/>
<motion.div
className={cn(
"fixed inset-y-0 z-50 w-80",
"bg-linear-to-b from-background/95 to-background/80",
"border-primary/10 backdrop-blur-md",
side === "left" ? "left-0 border-r" : "right-0 border-l",
"overflow-y-auto",
className,
)}
initial={{
x: side === "left" ? "-100%" : "100%",
opacity: 0,
}}
animate={{
x: 0,
opacity: 1,
}}
exit={{
x: side === "left" ? "-100%" : "100%",
opacity: 0,
}}
transition={{
type: "spring",
damping: 25,
stiffness: 200,
}}
>
<motion.button
onClick={handleToggle}
className={cn(
"group absolute right-4 top-4 z-20",
"rounded-full p-2",
"bg-primary/5 hover:bg-primary/10",
"transition-colors duration-200",
)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
>
<motion.div
className="absolute inset-0 rounded-full bg-primary/5"
initial={false}
animate={{
scale: [1, 1.2, 1],
opacity: [0.3, 0.2, 0.3],
}}
transition={{
duration: 3,
repeat: Infinity,
repeatType: "reverse",
}}
/>
<X className="h-4 w-4 text-primary/70" />
</motion.button>
<div
className={cn(
"absolute inset-0",
"bg-[url('/canvas-texture.png')] opacity-30",
"pointer-events-none",
)}
/>
<div className="relative h-full p-6">{children}</div>
</motion.div>
</>
)}
</AnimatePresence>
</>
);
}
Update the import paths to match your project setup.
import { CanvasDrawer } from "@/components/ui/canvas-drawer";
Usage
Basic Example
import { CanvasDrawer } from "@/components/ui/canvas-drawer";
export default function Example() {
const [open, setOpen] = React.useState(false);
return (
<CanvasDrawer side="right" open={open} onOpenChange={setOpen}>
<div className="p-4">
<h2>Drawer Content</h2>
<p>Click the artistic handle or close icon to toggle</p>
</div>
</CanvasDrawer>
);
}
Without Handle
Use showHandle={false}
to create a cleaner look without the artistic handle. The drawer can still be toggled using the close icon, clicking outside, or through external controls.
import { CanvasDrawer } from "@/components/ui/canvas-drawer";
import { Button } from "@/components/ui/button";
export default function Example() {
const [open, setOpen] = React.useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>Open Drawer</Button>
<CanvasDrawer
side="right"
open={open}
onOpenChange={setOpen}
showHandle={false}
>
<div className="p-4">
<h2>Drawer Content</h2>
<p>Use the close icon or click outside to dismiss</p>
</div>
</CanvasDrawer>
</>
);
}
Props
Prop | Type | Default | Description |
---|---|---|---|
children | React.ReactNode | - | The content to display inside the drawer |
open | boolean | false | Controls the open state of the drawer |
onOpenChange | (open: boolean) => void | - | Callback when drawer open state changes |
side | "left" | "right" | "right" | Which side the drawer appears from |
showHandle | boolean | true | Show the artistic handle/toggle button |
className | string | - | Additional CSS classes for styling |