Componentpickers1.23 KB gzipZero dependencies

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
import { DropdownMenuV2 } from "@/components/ui/dropdown-menu-v2";

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

Usage guidance

Use Dropdown Menu V2 when a ticket stub surface needs V2 dropdown menu: nested submenus, checkbox items, radio groups, separators, danger items, and shortcut kbd hints. Keep props minimal and prefer composition with sibling stubs.

Avoid Dropdown Menu V2 for long form flows or dense data grids where a dedicated layout block fits better.

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 -ms-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="ms-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="ms-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="ms-2 text-xs opacity-60">{item.shortcut}</span>
                    <span className="ms-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.