Drawer
ComponentsA panel that slides in smoothly from the edge of the screen, typically used for deep configuration, navigation drawers, or mobile menus.
Slide-Over Drawer
An overlay sheet pattern providing contextual space for forms, detailed inspections, and navigation menus without forcing a complete route change.
Live Functional Playground Interactive Hooks • useState
Drawer Configuration
Interactive drawer panel with escape key dismissal and focus preservation.
Motion Physics & Animation Showcase Framer Motion ✨
Motion Physics
✨
Hover to trigger spring elevation lift
Format:
Required Animation Library: framer-motion & lucide-react
Install this package in your project before importing the animated component.
npm i framer-motion lucide-react <div class="flex items-center justify-center">
<button type="button" class="px-4 py-2 text-xs font-semibold rounded-xl bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 transition-all">
Open Slide Drawer
</button>
</div> import React from "react";
export function Drawer({ isOpen = false, onClose, children }: { isOpen?: boolean; onClose?: () => void; children?: React.ReactNode }) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex justify-end bg-black/50 backdrop-blur-xs">
<div className="w-full max-w-md h-full bg-card p-6 shadow-2xl border-l border-border">
<button onClick={onClose} className="text-muted-foreground hover:text-foreground">Close</button>
{children}
</div>
</div>
);
} "use client";
import React, { useState, useEffect } from "react";
export interface DrawerProps {
title?: string;
description?: string;
triggerText?: string;
}
export default function Drawer({
title = "Project Settings",
description = "Configure your environment variables and deployment webhooks.",
triggerText = "Open Drawer Panel",
}: DrawerProps) {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") setIsOpen(false);
};
if (isOpen) {
document.addEventListener("keydown", handleKeyDown);
document.body.style.overflow = "hidden";
}
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.body.style.overflow = "unset";
};
}, [isOpen]);
return (
<div className="flex items-center justify-center">
<button
type="button"
onClick={() => setIsOpen(true)}
className="px-4 py-2 text-xs font-semibold rounded-xl bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 transition-all cursor-pointer"
>
{triggerText}
</button>
{isOpen && (
<div
className="fixed inset-0 z-50 bg-black/60 backdrop-blur-xs flex justify-end animate-in fade-in duration-200"
onClick={() => setIsOpen(false)}
>
<div
className="w-full max-w-md h-full bg-card text-card-foreground border-l border-border p-6 shadow-2xl flex flex-col justify-between animate-in slide-in-from-right duration-250"
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
>
<div className="space-y-4">
<div className="flex items-center justify-between pb-4 border-b border-border">
<div className="space-y-1">
<h3 className="font-bold text-base text-foreground">{title}</h3>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
<button
type="button"
onClick={() => setIsOpen(false)}
className="p-1 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted"
>
✕
</button>
</div>
<div className="space-y-3 py-2 text-xs">
<div className="space-y-1">
<label className="font-medium text-foreground">Environment Name</label>
<input
type="text"
defaultValue="production-main"
className="w-full px-3 py-2 rounded-lg border border-input bg-background"
/>
</div>
<div className="space-y-1">
<label className="font-medium text-foreground">Branch Strategy</label>
<input
type="text"
defaultValue="main -> auto-deploy"
className="w-full px-3 py-2 rounded-lg border border-input bg-background"
/>
</div>
</div>
</div>
<div className="flex justify-end gap-2 pt-4 border-t border-border">
<button
type="button"
onClick={() => setIsOpen(false)}
className="px-3.5 py-1.5 text-xs font-medium rounded-lg border border-border hover:bg-muted"
>
Cancel
</button>
<button
type="button"
onClick={() => setIsOpen(false)}
className="px-3.5 py-1.5 text-xs font-semibold rounded-lg bg-primary text-primary-foreground"
>
Save Preferences
</button>
</div>
</div>
</div>
)}
</div>
);
} "use client";
import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { PanelRightClose, X, Sparkles, Layers } from "lucide-react";
export default function AnimatedDrawer() {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="flex items-center justify-center">
<motion.button
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
onClick={() => setIsOpen(true)}
className="flex items-center gap-2 px-4 py-2.5 text-xs font-semibold rounded-xl bg-primary text-primary-foreground shadow-md hover:bg-primary/90 transition-colors"
>
<PanelRightClose className="h-4 w-4" />
<span>Open Animated Drawer</span>
</motion.button>
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-50 flex justify-end">
{/* Fade Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
onClick={() => setIsOpen(false)}
className="fixed inset-0 bg-black/60 backdrop-blur-xs"
/>
{/* Slide Drawer Panel */}
<motion.div
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ type: "spring", stiffness: 350, damping: 30 }}
className="relative z-10 w-full max-w-md h-full bg-card/95 backdrop-blur-md border-l border-border p-6 shadow-2xl flex flex-col justify-between"
role="dialog"
aria-modal="true"
>
<div className="space-y-5">
<div className="flex items-center justify-between pb-4 border-b border-border">
<div className="flex items-center gap-2">
<Layers className="h-5 w-5 text-primary" />
<h3 className="font-bold text-base text-foreground">Drawer Navigation</h3>
</div>
<button
type="button"
onClick={() => setIsOpen(false)}
className="p-1 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="space-y-3">
<div className="p-4 rounded-xl border border-primary/20 bg-primary/5 flex items-start gap-3">
<Sparkles className="h-5 w-5 text-primary shrink-0 mt-0.5" />
<div>
<h4 className="text-xs font-bold text-foreground">Spring Physics Animation</h4>
<p className="text-[11px] text-muted-foreground mt-0.5">
Fluid transitions powered by Framer Motion AnimatePresence and hardware acceleration.
</p>
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-2 pt-4 border-t border-border">
<button
type="button"
onClick={() => setIsOpen(false)}
className="px-4 py-2 text-xs font-medium rounded-xl border border-border hover:bg-muted transition-colors"
>
Dismiss
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
} Props Reference
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | "Project Settings" | Header title rendered at the top of the drawer |
description | string | "" | Subheading or explanatory helper text |
triggerText | string | "Open Drawer Panel" | Button label for opening the drawer |
isOpen | boolean | false | Controlled open state |
onClose | () => void | undefined | Callback fired on dismissal |
Focus Trapping & Accessibility
- Keyboard Escape: Pressing the
Escapekey automatically closes the sheet and restores focus. - Scroll Lock: Automatically prevents background document scrolling (
body overflow: hidden) while the drawer is open. - Backdrop Clicks: Clicking anywhere outside the content panel cleanly dismisses the drawer.