Componentnavigation1.46 KB gzipZero dependencies

Menubar

Theater box office menu bar with dropdown menus, shortcuts, and dashed frames.

Preview

Live Preview
Click any menu header to open dropdown

Installation

Tokens guide →

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

npx bigbullui add menubar

Or install the full npm package:

npm install bigbullui
import { Menubar } from "@/components/ui/menubar";

<Menubar
  menus={[
    { id: "file", label: "Ticket", items: [{ id: "p", label: "Print", shortcut: "⌘P" }] },
  ]}
/>

Usage guidance

Use Menubar when a ticket stub surface needs Theater box office menu bar with dropdown menus, shortcuts, and dashed frames. Keep props minimal and prefer composition with sibling stubs.

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

Source code

menubar.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 interface MenubarItem {
  id: string;
  label: string;
  shortcut?: string;
  disabled?: boolean;
  danger?: boolean;
  onSelect?: () => void;
}

export interface MenubarMenu {
  id: string;
  label: string;
  items: (MenubarItem | "separator")[];
}

export interface MenubarProps extends React.HTMLAttributes<HTMLDivElement> {
  menus: MenubarMenu[];
  className?: string;
  /** Dual-mode: false renders instantly with no motion. Default true. */
  animated?: boolean;
}

export function Menubar({ menus, className, animated = true, ...props }: MenubarProps) {
  const [activeMenuId, setActiveMenuId] = React.useState<string | null>(null);
  const containerRef = React.useRef<HTMLDivElement>(null);

  React.useEffect(() => {
    if (!activeMenuId) return;
    const handleClickOutside = (e: MouseEvent) => {
      if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
        setActiveMenuId(null);
      }
    };
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === "Escape") setActiveMenuId(null);
    };

    document.addEventListener("mousedown", handleClickOutside);
    document.addEventListener("keydown", handleKeyDown);
    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
      document.removeEventListener("keydown", handleKeyDown);
    };
  }, [activeMenuId]);

  return (
    <div
      ref={containerRef}
      role="menubar"
      className={cn(
        "inline-flex items-center rounded-lg border-2 border-foreground bg-card p-1 shadow-xs font-mono select-none",
        className
      )}
      {...props}
    >
      {menus.map((menu) => {
        const isOpen = activeMenuId === menu.id;

        return (
          <div key={menu.id} className="relative">
            <button
              type="button"
              role="menuitem"
              aria-haspopup="true"
              aria-expanded={isOpen}
              onClick={() => setActiveMenuId(isOpen ? null : menu.id)}
              onMouseEnter={() => {
                if (activeMenuId !== null) setActiveMenuId(menu.id);
              }}
              className={cn(
                "rounded-md px-3 py-1 text-xs font-bold uppercase tracking-wider transition-colors cursor-pointer",
                isOpen
                  ? "bg-foreground text-background"
                  : "text-foreground hover:bg-secondary"
              )}
            >
              {menu.label}
            </button>

            {isOpen && (
              <div
                role="menu"
                className={cn("absolute left-0 top-full z-50 mt-1 min-w-[170px] rounded-lg border-2 border-foreground bg-card p-1.5 shadow-xl outline-1 outline-dashed outline-offset-[-4px] text-xs", animated && "animate-[scale-in_0.12s_ease-out_both]")}
              >
                {menu.items.map((item, idx) => {
                  if (item === "separator") {
                    return (
                      <div
                        key={`sep-${idx}`}
                        className="my-1 border-t border-dashed border-border"
                      />
                    );
                  }

                  return (
                    <button
                      key={item.id}
                      type="button"
                      disabled={item.disabled}
                      onClick={() => {
                        item.onSelect?.();
                        setActiveMenuId(null);
                      }}
                      className={cn(
                        "flex w-full cursor-pointer items-center justify-between rounded-sm px-2.5 py-1.5 text-start text-xs transition-colors",
                        item.danger
                          ? "text-destructive hover:bg-destructive hover:text-white"
                          : "text-foreground hover:bg-accent hover:text-accent-foreground",
                        item.disabled && "opacity-40 pointer-events-none cursor-not-allowed"
                      )}
                    >
                      <span>{item.label}</span>
                      {item.shortcut && (
                        <span className="text-[10px] opacity-70 tracking-wider">
                          {item.shortcut}
                        </span>
                      )}
                    </button>
                  );
                })}
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}

Props

PropTypeDescription
menusMenubarMenu[]
className?string
animated?booleanDual-mode: false renders instantly with no motion. Default true.