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.jiawei.dev
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 |
---|---|
![]() |
![]() |
![]() |
![]() |
The easiest way to install DropDrawer is through the shadcn registry:
# pnpm
pnpm dlx shadcn@latest add https://dropdrawer.jiawei.dev/r/dropdrawer.json
# npm
npx shadcn@latest add https://dropdrawer.jiawei.dev/r/dropdrawer.json
# yarn
yarn dlx shadcn@latest add https://dropdrawer.jiawei.dev/r/dropdrawer.json
# bun
bunx shadcn@latest add https://dropdrawer.jiawei.dev/r/dropdrawer.json
During local development, you can use:
npx shadcn@latest add http://localhost:3000/r/dropdrawer.json
This will automatically:
- Install all required dependencies
- Add the component to your project
- Set up all necessary configuration
- Copy the
dropdown-menu
anddrawer
components 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 drawer
Alternatively, 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
useIsMobile
hook:
Click to show code
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}
- Copy the
dropdrawer
component:
Click to show code
"use client";
import { ChevronRightIcon } from "lucide-react";
import * as React from "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 repository
-
Update 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 DropdownMenu
import { 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";
export function ModeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="rounded-full">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
// After: Using DropDrawer
import { 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";
export function ModeToggle() {
const { setTheme } = useTheme();
return (
<DropDrawer>
<DropDrawerTrigger asChild>
<Button variant="outline" size="icon" className="rounded-full">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropDrawerTrigger>
<DropDrawerContent align="end">
<DropDrawerItem onClick={() => setTheme("light")}>Light</DropDrawerItem>
<DropDrawerItem onClick={() => setTheme("dark")}>Dark</DropDrawerItem>
<DropDrawerItem onClick={() => 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";
export function Example() {
return (
<DropDrawer>
<DropDrawerTrigger asChild>
<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 * as React from "react";
import {
DropDrawer,
DropDrawerContent,
DropDrawerItem,
DropDrawerTrigger,
} from "@/components/ui/dropdrawer";
import { Button } from "@/components/ui/button";
export function StateExample() {
const [open, setOpen] = React.useState(false);
const handleOpen = () => {
setOpen(true);
};
return (
<>
<Button onClick={handleOpen}>Open with State</Button>
<DropDrawer open={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";
export function NestedExample() {
return (
<DropDrawer>
<DropDrawerTrigger asChild>
<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";
export function PostExample() {
const [open, setOpen] = useState(false);
return (
<DropDrawer open={open} onOpenChange={setOpen}>
<DropDrawerTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-full">
<MoreVertical className="h-5 w-5" />
</Button>
</DropDrawerTrigger>
<DropDrawerContent>
{/* First group: Add to feed, Save, Not interested */}
<DropDrawerGroup>
<DropDrawerSub>
<DropDrawerSubTrigger
icon={<LayoutDashboard className="h-5 w-5" />}
>
Add to feed
</DropDrawerSubTrigger>
<DropDrawerSubContent>
<DropDrawerItem icon={<Home className="h-5 w-5" />}>
Home
</DropDrawerItem>
<DropDrawerItem icon={<LayoutDashboard className="h-5 w-5" />}>
Work
</DropDrawerItem>
<DropDrawerItem icon={<BookmarkIcon className="h-5 w-5" />}>
Personal
</DropDrawerItem>
</DropDrawerSubContent>
</DropDrawerSub>
<DropDrawerItem icon={<BookmarkIcon className="h-5 w-5" />}>
Save
</DropDrawerItem>
<DropDrawerItem icon={<EyeOffIcon className="h-5 w-5" />}>
Not interested
</DropDrawerItem>
</DropDrawerGroup>
<DropDrawerSeparator />
{/* Second group */}
<DropDrawerGroup>
<DropDrawerItem icon={<UserMinusIcon className="h-5 w-5" />}>
Mute
</DropDrawerItem>
<DropDrawerItem icon={<Copy className="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