Skip to content

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.

Notifications You must be signed in to change notification settings

jiaweing/DropDrawer

Repository files navigation

DropDrawer

MIT License Version

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

Why DropDrawer?

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)

Desktop View

Light Mode Dark Mode
Desktop dropdown view Desktop dropdown view dark

Mobile Views

Main Drawer Submenu Navigation
Mobile drawer view Mobile drawer with submenu
Mobile drawer view dark Mobile drawer with submenu dark

Installation

Using shadcn registry (Recommended)

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

Manual Installation

  1. Copy the dropdown-menu and drawer 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
  1. 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;
}
  1. 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
  1. Update the import paths based on your project structure.

  2. 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.

Usage

Migrating from DropdownMenu

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:

  1. Replace imports:
- import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
+ import { DropDrawer, DropDrawerContent, DropDrawerItem, DropDrawerTrigger } from "@/components/ui/dropdrawer";
  1. 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.

Real-world Example: Theme Switcher

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>
  );
}

Basic Example

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>
  );
}

Using State to Control the Menu

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>
    </>
  );
}

Advanced Usage with Nested Submenus

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>
  );
}

Complex Example with Groups and Icons

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>
  );
}

API Reference

Component Overview

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

DropDrawer

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

DropDrawerTrigger

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

DropDrawerContent

The content of the dropdown/drawer.

Prop Type Description
className string Additional CSS classes
children React.ReactNode The content of the dropdown/drawer

DropDrawerItem

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

DropDrawerSub

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)

DropDrawerFooter

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

Credits

About

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.

Resources

Code of conduct

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published