A responsive component that automatically switches between a dropdown menu on desktop and a drawer on mobile devices for shadcn/ui.
A drop-in replacement for shadcn/ui's DropdownMenu component.
Demo:https://dropdrawer.jiaweing.com
Traditional dropdown menus don't feel native on mobile devices - they can be difficult to interact with on small screens and often create a poor user experience.
DropDrawer solves this problem by:
- Automatically switching between dropdown menus on desktop
- Using a native-feeling drawer interface on mobile devices
- Providing consistent interaction patterns across all screen sizes
- Using a responsive breakpoint of 768px (configurable via the hook)
| Light Mode | Dark Mode |
|---|---|
![]() | ![]() |
| Main Drawer | Submenu Navigation |
|---|---|
![]() | ![]() |
![]() | ![]() |
shadcn can install DropDrawer straight from this GitHub repository - no registry server or JSON URL required:
# pnpm
pnpm dlx shadcn@latest add jiaweing/DropDrawer/dropdrawer
# npm
npx shadcn@latest add jiaweing/DropDrawer/dropdrawer
# yarn
yarn dlx shadcn@latest add jiaweing/DropDrawer/dropdrawer
# bun
bunx shadcn@latest add jiaweing/DropDrawer/dropdrawerYou can pin to a branch, tag, or commit SHA with #ref:
npx shadcn@latest add jiaweing/DropDrawer/dropdrawer#mainYou can also install from the hosted registry JSON:
# pnpm
pnpm dlx shadcn@latest add https://dropdrawer.jiaweing.com/r/dropdrawer.json
# npm
npx shadcn@latest add https://dropdrawer.jiaweing.com/r/dropdrawer.json
# yarn
yarn dlx shadcn@latest add https://dropdrawer.jiaweing.com/r/dropdrawer.json
# bun
bunx shadcn@latest add https://dropdrawer.jiaweing.com/r/dropdrawer.jsonDuring local development, you can use:
npx shadcn@latest add http://localhost:3000/r/dropdrawer.jsonEither method will automatically:
- Install all required dependencies
- Add the component to your project
- Set up all necessary configuration
- Copy the
dropdown-menuanddrawercomponents from shadcn/ui.
# pnpm
pnpm dlx shadcn@latest add dropdown-menu drawer
# npm
npx shadcn@latest add dropdown-menu drawer
# yarn
yarn dlx shadcn@latest add dropdown-menu drawer
# bun
bunx shadcn@latest add dropdown-menu drawerAlternatively, if you are not using shadcn/ui cli, you can manually copy the components from shadcn/ui.
If you copied the drawer component manually, make sure to install vaul:
npm install vaul- Copy the
useIsMobilehook:
Click to show code
import*asReactfrom"react";constMOBILE_BREAKPOINT=768;exportfunctionuseIsMobile(){const[isMobile,setIsMobile]=React.useState<boolean|undefined>(undefined);React.useEffect(()=>{constmql=window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT-1}px)`);constonChange=()=>{setIsMobile(window.innerWidth<MOBILE_BREAKPOINT);};mql.addEventListener("change",onChange);setIsMobile(window.innerWidth<MOBILE_BREAKPOINT);return()=>mql.removeEventListener("change",onChange);},[]);return!!isMobile;}- Copy the
dropdrawercomponent:
Click to show code
"use client";import{ChevronRightIcon}from"lucide-react";import*asReactfrom"react";import{Drawer,DrawerClose,DrawerContent,DrawerFooter,DrawerHeader,DrawerTitle,DrawerTrigger,}from"@/components/ui/drawer";import{DropdownMenu,DropdownMenuContent,DropdownMenuItem,DropdownMenuLabel,DropdownMenuSeparator,DropdownMenuSub,DropdownMenuSubContent,DropdownMenuSubTrigger,DropdownMenuTrigger,}from"@/components/ui/dropdown-menu";import{useIsMobile}from"@/hooks/use-mobile";import{cn}from"@/lib/utils";// Component code here - see full implementation in the repositoryUpdate the import paths based on your project structure.
Customize the mobile breakpoint (optional):
The default mobile breakpoint is 768px. You can customize this by modifying the MOBILE_BREAKPOINT constant in the use-mobile.ts hook.
DropDrawer is designed as a drop-in replacement for shadcn/ui's DropdownMenu component. You can easily migrate your existing DropdownMenu components to DropDrawer with a simple find-and-replace:
- Replace imports:
- import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";+ import { DropDrawer, DropDrawerContent, DropDrawerItem, DropDrawerTrigger } from "@/components/ui/dropdrawer";- Replace component names:
- <DropdownMenu>- <DropdownMenuTrigger>- <DropdownMenuContent>- <DropdownMenuItem>+ <DropDrawer>+ <DropDrawerTrigger>+ <DropDrawerContent>+ <DropDrawerItem>The component API is designed to match DropdownMenu, so most props should work without changes.
Here's the mapping between DropdownMenu and DropDrawer components:
| DropdownMenu Component | DropDrawer Component |
|---|---|
DropdownMenu | DropDrawer |
DropdownMenuTrigger | DropDrawerTrigger |
DropdownMenuContent | DropDrawerContent |
DropdownMenuItem | DropDrawerItem |
DropdownMenuLabel | DropDrawerLabel |
DropdownMenuSeparator | DropDrawerSeparator |
DropdownMenuGroup | DropDrawerGroup |
DropdownMenuSub | DropDrawerSub |
DropdownMenuSubTrigger | DropDrawerSubTrigger |
DropdownMenuSubContent | DropDrawerSubContent |
Note: Some advanced DropdownMenu components like DropdownMenuCheckboxItem, DropdownMenuRadioGroup, and DropdownMenuRadioItem are not currently implemented in DropDrawer.
Here's how to convert a theme switcher from DropdownMenu to DropDrawer:
// Before: Using DropdownMenuimport{Moon,Sun}from"lucide-react";import{useTheme}from"@/components/theme-provider";import{Button}from"@/components/ui/button";import{DropdownMenu,DropdownMenuContent,DropdownMenuItem,DropdownMenuTrigger,}from"@/components/ui/dropdown-menu";exportfunctionModeToggle(){const{ setTheme }=useTheme();return(<DropdownMenu><DropdownMenuTriggerasChild><Buttonvariant="outline"size="icon"className="rounded-full"><SunclassName="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"/><MoonclassName="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"/><spanclassName="sr-only">Toggle theme</span></Button></DropdownMenuTrigger><DropdownMenuContentalign="end"><DropdownMenuItemonClick={()=>setTheme("light")}>
Light
</DropdownMenuItem><DropdownMenuItemonClick={()=>setTheme("dark")}>
Dark
</DropdownMenuItem><DropdownMenuItemonClick={()=>setTheme("system")}>
System
</DropdownMenuItem></DropdownMenuContent></DropdownMenu>);}// After: Using DropDrawerimport{Moon,Sun}from"lucide-react";import{useTheme}from"@/components/theme-provider";import{Button}from"@/components/ui/button";import{DropDrawer,DropDrawerContent,DropDrawerItem,DropDrawerTrigger,}from"@/components/ui/dropdrawer";exportfunctionModeToggle(){const{ setTheme }=useTheme();return(<DropDrawer><DropDrawerTriggerasChild><Buttonvariant="outline"size="icon"className="rounded-full"><SunclassName="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"/><MoonclassName="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"/><spanclassName="sr-only">Toggle theme</span></Button></DropDrawerTrigger><DropDrawerContentalign="end"><DropDrawerItemonClick={()=>setTheme("light")}>Light</DropDrawerItem><DropDrawerItemonClick={()=>setTheme("dark")}>Dark</DropDrawerItem><DropDrawerItemonClick={()=>setTheme("system")}>
System
</DropDrawerItem></DropDrawerContent></DropDrawer>);}Create a simple dropdown/drawer menu with just a few lines of code:
import{DropDrawer,DropDrawerContent,DropDrawerItem,DropDrawerTrigger,}from"@/components/ui/dropdrawer";import{Button}from"@/components/ui/button";exportfunctionExample(){return(<DropDrawer><DropDrawerTriggerasChild><Button>Open Menu</Button></DropDrawerTrigger><DropDrawerContent><DropDrawerItem>Item 1</DropDrawerItem><DropDrawerItem>Item 2</DropDrawerItem><DropDrawerItem>Item 3</DropDrawerItem></DropDrawerContent></DropDrawer>);}You can control the open state of the menu using React state:
import*asReactfrom"react";import{DropDrawer,DropDrawerContent,DropDrawerItem,DropDrawerTrigger,}from"@/components/ui/dropdrawer";import{Button}from"@/components/ui/button";exportfunctionStateExample(){const[open,setOpen]=React.useState(false);consthandleOpen=()=>{setOpen(true);};return(<><ButtononClick={handleOpen}>Open with State</Button><DropDraweropen={open}onOpenChange={setOpen}><DropDrawerContent><DropDrawerItem>Item 1</DropDrawerItem><DropDrawerItem>Item 2</DropDrawerItem><DropDrawerItem>Item 3</DropDrawerItem></DropDrawerContent></DropDrawer></>);}Create complex navigation structures with nested submenus:
import{DropDrawer,DropDrawerContent,DropDrawerItem,DropDrawerSub,DropDrawerSubContent,DropDrawerSubTrigger,DropDrawerTrigger,}from"@/components/ui/dropdrawer";import{Button}from"@/components/ui/button";exportfunctionNestedExample(){return(<DropDrawer><DropDrawerTriggerasChild><Button>Open Menu</Button></DropDrawerTrigger><DropDrawerContent><DropDrawerItem>Item 1</DropDrawerItem><DropDrawerSub><DropDrawerSubTrigger>Submenu</DropDrawerSubTrigger><DropDrawerSubContent><DropDrawerItem>Submenu Item 1</DropDrawerItem><DropDrawerItem>Submenu Item 2</DropDrawerItem></DropDrawerSubContent></DropDrawerSub><DropDrawerItem>Item 3</DropDrawerItem></DropDrawerContent></DropDrawer>);}Here's a more complex example showing how to use groups, icons, and multiple nested submenus:
import{AlertTriangle,BookmarkIcon,Copy,EyeOffIcon,Home,LayoutDashboard,MoreVertical,Settings,ShieldIcon,UserMinusIcon,UserXIcon,}from"lucide-react";import{useState}from"react";import{Button}from"@/components/ui/button";import{DropDrawer,DropDrawerContent,DropDrawerGroup,DropDrawerItem,DropDrawerSeparator,DropDrawerSub,DropDrawerSubContent,DropDrawerSubTrigger,DropDrawerTrigger,}from"@/components/ui/dropdrawer";exportfunctionPostExample(){const[open,setOpen]=useState(false);return(<DropDraweropen={open}onOpenChange={setOpen}><DropDrawerTriggerasChild><Buttonvariant="ghost"size="icon"className="h-8 w-8 rounded-full"><MoreVerticalclassName="h-5 w-5"/></Button></DropDrawerTrigger><DropDrawerContent>{/* First group: Add to feed, Save, Not interested */}<DropDrawerGroup><DropDrawerSub><DropDrawerSubTriggericon={<LayoutDashboardclassName="h-5 w-5"/>}>
Add to feed
</DropDrawerSubTrigger><DropDrawerSubContent><DropDrawerItemicon={<HomeclassName="h-5 w-5"/>}>
Home
</DropDrawerItem><DropDrawerItemicon={<LayoutDashboardclassName="h-5 w-5"/>}>
Work
</DropDrawerItem><DropDrawerItemicon={<BookmarkIconclassName="h-5 w-5"/>}>
Personal
</DropDrawerItem></DropDrawerSubContent></DropDrawerSub><DropDrawerItemicon={<BookmarkIconclassName="h-5 w-5"/>}>
Save
</DropDrawerItem><DropDrawerItemicon={<EyeOffIconclassName="h-5 w-5"/>}>
Not interested
</DropDrawerItem></DropDrawerGroup><DropDrawerSeparator/>{/* Second group */}<DropDrawerGroup><DropDrawerItemicon={<UserMinusIconclassName="h-5 w-5"/>}>
Mute
</DropDrawerItem><DropDrawerItemicon={<CopyclassName="h-5 w-5"/>}>
Copy link
</DropDrawerItem></DropDrawerGroup></DropDrawerContent></DropDrawer>);}| Component | Description |
|---|---|
DropDrawer | Root component that manages state |
DropDrawerTrigger | Button that opens the dropdown/drawer |
DropDrawerContent | Container for dropdown/drawer content |
DropDrawerItem | Individual menu item |
DropDrawerSub | Container for submenu |
DropDrawerSubTrigger | Button that opens a submenu |
DropDrawerSubContent | Container for submenu content |
DropDrawerSeparator | Visual separator between items |
DropDrawerGroup | Groups related menu items |
DropDrawerFooter | Footer section for the drawer |
The root component that manages the state of the dropdown/drawer.
| Prop | Type | Description |
|---|---|---|
open | boolean | Controls the open state |
onOpenChange | (open: boolean) => void | Callback when open state changes |
children | React.ReactNode | The content of the component |
The button that triggers the dropdown/drawer.
| Prop | Type | Description |
|---|---|---|
asChild | boolean | Whether to merge props with the child element |
children | React.ReactNode | The content of the trigger |
The content of the dropdown/drawer.
| Prop | Type | Description |
|---|---|---|
className | string | Additional CSS classes |
children | React.ReactNode | The content of the dropdown/drawer |
An item in the dropdown/drawer.
| Prop | Type | Description |
|---|---|---|
onSelect | (event: Event) => void | Callback when item is selected |
onClick | React.MouseEventHandler<HTMLDivElement> | Callback when item is clicked |
icon | React.ReactNode | Icon to display on the left side of item |
variant | "default" | "destructive" | Visual style variant |
inset | boolean | Whether to add left padding |
disabled | boolean | Whether the item is disabled |
children | React.ReactNode | The content of the item |
A container for submenu content.
| Prop | Type | Description |
|---|---|---|
id | string | Optional ID for the submenu (auto-generated if not provided) |
children | React.ReactNode | The content of the submenu (should include SubTrigger and SubContent) |
A footer section for the drawer. This component is only rendered in mobile mode (drawer) and is ignored in desktop mode (dropdown).
| Prop | Type | Description |
|---|---|---|
className | string | Additional CSS classes |
children | React.ReactNode | The content of the footer |
- shadcn/ui by shadcn
- Vaul by emilkowalski
- Radix UI by Workos
- Credenza by redpangilinan for the inspiration





