bigbullui logobigbullui
Componentpickers

Dropdown Menu V2

V2 dropdown menu: nested submenus, checkbox items, radio groups, separators, danger items, and shortcut kbd hints.

Preview

Live Preview
⌘NNew project
⌘OOpen…
⌘1Project 1
⌘2Project 2
⌘3Project 3
Export
Dangerous action
Edit settings

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add dropdown-menu-v2

Or install the full npm package:

npm install bigbullui

Usage

import { DropdownMenuV2 } from "@/components/ui/dropdown-menu-v2";

<DropdownMenuV2 items={[{ id: "new", label: "New", shortcut: "⌘N" }, { id: "danger", label: "Delete", danger: true }]} />

Source code

dropdown-menu-v2.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export type DropdownMenuItem = DropdownMenuV2Item;
export type DropdownMenuV2Item = {
  id: string;
  label: string;
  disabled?: boolean;
  shortcut?: string;
  separator?: boolean;
  submenu?: DropdownMenuV2Item[];
  checkbox?: boolean;
  radioGroup?: string;
  radioValue?: string;
  danger?: boolean;
};

export type DropdownMenuV2Props = {
  items: DropdownMenuV2Item[];
  className?: string;
};

const dropdownMenuV2Shake = `
  @keyframes dropdownMenuV2Shake {
    0%, 100% { transform: translateX(0); }
    20%, 60%, 80% { transform: translateX(-4px); }
    40%, 100% { transform: translateX(4px); }
  }
`;
<style>{dropdownMenuV2Shake}</style>;

const DropdownMenuV2 = React.forwardRef<HTMLDivElement, DropdownMenuV2Props>(
  ({ items, className }, ref) => {
    return (
      <div
        ref={ref}
        className={cn(
          "w-sm rounded-md bg-card border border-input p-1 shadow-sm",
          "motion-reduce:transition-none",
          className
        )}
      >
        <div className="flex flex-col space-y-1">
          {items.map((item) => {
            if (item.separator) {
              return (
                <div
                  key={item.id}
                  className={cn("h-px my-2 border-t border-border/50", "motion-reduce:transition-none")}
                />
              );
            }

            if (item.submenu && item.submenu.length > 0) {
              return (
                <div key={item.id} className="group">
                  <div
                    onClick={() => {}}
                    role="button"
                    aria-haspopup="true"
                    aria-expanded="false"
                    className={cn(
                      "flex items-center rounded-md px-2 py-1.5 hover:bg-accent/10 hover:text-accent-foreground cursor-pointer",
                      "motion-reduce:transition-none"
                    )}
                  >
                    {item.label}
                    <svg
                      width="8"
                      height="8"
                      viewBox="0 0 24 24"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth={2}
                      className="mt-0.5 -ml-0.5 transition-transform group-open:rotate-180"
                    >
                      <path d="M6 9l6 6 6-6" />
                    </svg>
                  </div>
                  <div
                    className={cn(
                      "absolute left-full w-48 mt-1.5 rounded-md bg-card border border-input shadow-origin origin-topRight origin-topRightAuto pt-1",
                      "motion-reduce:transition-none"
                    )}
                  >
                    <DropdownMenuV2 items={item.submenu} />
                  </div>
                </div>
              );
            }

            if (item.checkbox) {
              return (
                <div
                  key={item.id}
                  className={cn("flex items-center rounded-md px-2 py-1.5 cursor-select-none", "motion-reduce:transition-none")}
                >
                  <input
                    type="checkbox"
                    className={cn("w-4 h-4 rounded border border-primary bg-primary/10 cursor-pointer", "motion-reduce:transition-none")}
                  />
                  <span className="ml-2 text-sm">{item.label}</span>
                </div>
              );
            }

            if (item.radioGroup) {
              return (
                <div
                  key={item.id}
                  className={cn("flex items-center rounded-md px-2 py-1.5 cursor-pointer", "motion-reduce:transition-none")}
                >
                  <div
                    className={cn(
                      "w-3 h-3 rounded-full border border-primary bg-primary/10 flex-shrink-0",
                      "motion-reduce:transition-none"
                    )}
                  />
                  <span className="ml-2 text-sm flex-1">{item.label}</span>
                </div>
              );
            }

            return (
              <div
                key={item.id}
                className={cn(
                  "flex items-center rounded-md px-2 py-1.5 cursor-pointer select-none",
                  item.danger && "bg-accent/10 text-accent-foreground",
                  "motion-reduce:transition-none"
                )}
                onClick={() => {}}
              >
                {item.shortcut ? (
                  <>
                    <span className="ml-2 text-xs opacity-60">{item.shortcut}</span>
                    <span className="ml-2 text-sm">{item.label}</span>
                  </>
                ) : (
                  item.label
                )}
              </div>
            );
          })}
        </div>
      </div>
    );
  }
);
DropdownMenuV2.displayName = "DropdownMenuV2";

export { DropdownMenuV2 };

Props

PropTypeDescription
itemsDropdownMenuItem[]Menu items with submenu/checkbox/radio/danger support.