Responsive Drawer
A responsive drawer component built with Vaul and Framer Motion. Opens like an app drawer on mobile devices and functions as a dialog or sliding panel on desktop.
A responsive drawer component built with Vaul and Framer Motion. Opens like an app drawer on mobile devices and functions as a dialog or sliding panel on desktop.
npm install motion vaul
please add this property in your layout.js or app.js
1export default function RootLayout({2children,3}: Readonly<{4children: React.ReactNode;5}>) {6return (7<html lang='en'>8<body className={poppins.className}>9<div vaul-drawer-wrapper=''>{children}</div>10</body>11</html>12);13}
'use client';import React, {createContext,useContext,useState,useEffect,ReactNode,} from 'react';import { createPortal } from 'react-dom';import { AnimatePresence, motion } from 'motion/react';import { X } from 'lucide-react';import { Drawer as VaulDrawer } from 'vaul';import { cn } from '@/lib/utils';interface DrawerContextProps {open: boolean;setOpen: (open: boolean) => void;}const DrawerContext = createContext<DrawerContextProps | undefined>(undefined);export const useDrawer = () => {const context = useContext(DrawerContext);if (!context) {throw new Error('useDrawer must be used within a DrawerProvider');}return context;};interface ResponsiveDrawerProps {children: ReactNode;triggerContent?: ReactNode;open?: boolean;setOpen?: (open: boolean) => void;closeBtnClass?: string;classname?: string;}export function ResponsiveDrawer({children,open: controlledOpen,setOpen: controlledSetOpen,classname,closeBtnClass,}: ResponsiveDrawerProps) {const [internalOpen, setInternalOpen] = useState(false);const [isDesktop, setIsDesktop] = useState(false);const [mounted, setMounted] = useState(false);const open = controlledOpen !== undefined ? controlledOpen : internalOpen;const setOpen = controlledSetOpen || setInternalOpen;useEffect(() => {setMounted(true);const mediaQuery = window.matchMedia('(min-width: 768px)');const handleMediaChange = (event: MediaQueryListEvent) => {setIsDesktop(event.matches);};setIsDesktop(mediaQuery.matches);mediaQuery.addEventListener('change', handleMediaChange);return () => mediaQuery.removeEventListener('change', handleMediaChange);}, []);const trigger = React.Children.toArray(children).find((child: any) => child.type === DrawerTrigger);const content = React.Children.toArray(children).filter((child: any) => child.type !== DrawerTrigger);const desktopModal =isDesktop && mounted? createPortal(<AnimatePresence>{open && (<motion.divinitial={{ opacity: 0 }}animate={{ opacity: 1 }}exit={{ opacity: 0 }}className='fixed inset-0 z-50] flex items-center justify-center bg-black/50 backdrop-blur-sm cursor-zoom-out'onClick={() => setOpen(false)}><motion.divinitial={{ scale: 0.9, opacity: 0 }}animate={{ scale: 1, opacity: 1 }}exit={{ scale: 0.9, opacity: 0 }}transition={{ type: 'spring', duration: 0.5 }}onClick={(e) => e.stopPropagation()}className={cn('relative w-full max-w-md border bg-white dark:bg-neutral-900 rounded-lg cursor-default',classname)}><buttonclassName={cn('absolute top-2 right-2 bg-primary text-background p-2 border z-[1] rounded-md',closeBtnClass)}onClick={() => setOpen(false)}><X /></button>{content}</motion.div></motion.div>)}</AnimatePresence>,document.body): null;return (<DrawerContext.Provider value={{ open, setOpen }}>{trigger}{desktopModal}{!isDesktop && (<VaulDrawer.RootshouldScaleBackgroundopen={open}onOpenChange={setOpen}><VaulDrawer.Portal><VaulDrawer.Overlay className='fixed inset-0 z-50 bg-white/50 dark:bg-black/50 backdrop-blur-sm' /><VaulDrawer.Content className='fixed bottom-0 left-0 z-50 w-full max-h-[96%] bg-white dark:bg-neutral-900'><div className='mx-auto w-16 h-[0.30rem] flex-shrink-0 rounded-full bg-neutral-600 my-4' /><div className='w-full mx-auto max-h-[96vh] overflow-auto px-4 pb-2'>{content}</div></VaulDrawer.Content></VaulDrawer.Portal></VaulDrawer.Root>)}</DrawerContext.Provider>);}export function DrawerContent({children,className,}: {children: ReactNode;className?: string;}) {return <div className={cn('', className)}>{children}</div>;}export function DrawerTrigger({ children }: { children: ReactNode }) {const { setOpen } = useDrawer();return <div onClick={() => setOpen(true)}>{children}</div>;}
Prop | Type | Description |
---|---|---|
open | boolean | The content to be displayed within the AuroraBackground component. |
setOpen | boolean | this is an function to close and open the drawer |
triggerContent | ReactNode | This will use for default click without an using state |