bigbullui logobigbullui
Componentnavigation

FlyoutMenu

Hover dropdown menu with scale-in origin-top panel and keyboard navigation

Preview

Live Preview
Menu

Installation

Tokens guide →

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

npx bigbullui add flyout-menu

Or install the full npm package:

npm install bigbullui

Usage

import { FlyoutMenu } from "@/components/ui/flyout-menu"

<FlyoutMenu trigger={<div>Menu</div>} items={items} onSelect={handleSelect} />

Source code

flyout-menu.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 FlyoutMenuItem {
  id: string;
  label: string;
  shortcut?: string;
}

export interface FlyoutMenuProps {
  trigger: React.ReactNode;
  items: FlyoutMenuItem[];
  onSelect?: (id: string) => void;
  className?: string;
}

export function FlyoutMenu({
  trigger,
  items,
  onSelect,
  className,
}: FlyoutMenuProps) {
  const [open, setOpen] = React.useState(false);

  const handleKeyDown = React.useCallback(
    (event: React.KeyboardEvent) => {
      if (event.key === "Escape") {
        setOpen(false);
      } else if (event.key === "ArrowDown") {
        setOpen(true);
      }
    },
    []
  );

  const handleBlur = React.useCallback(
    (event: React.FocusEvent) => {
      if (!event.relatedTarget?.closest(`.${cn("flyout-panel")}`)) {
        setOpen(false);
      }
    },
    []
  );

  React.useEffect(() => {
    return () => {
      // cleanup
    };
  }, []);

  return (
    <div className="relative inline-block" onBlur={handleBlur}>
      {trigger}

      {open && (
        <div
          className={cn(
            "absolute top-full left-0 mt-2 w-64 rounded-lg bg-card border border-border/60 shadow-lg p-2 sm:p-3 overflow-hidden animate-[scale-in_0.2s_ease-out_both]",
            "origin-top"
          )}
        >
          {items.map((item) => {
            const handleClick = React.useCallback(() => {
              onSelect?.(item.id);
              setOpen(false);
            }, [onSelect, item.id]);

            return (
              <button
                key={item.id}
                className={cn(
                  "flex items-center gap-2 rounded-sm px-2 py-1 text-sm text-foreground hover:bg-secondary/60 hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
                  "aria-disabled=" + item.id
                )}
                onClick={handleClick}
                onFocus={handleBlur}
                role="menuitem"
              >
                <span className="font-mono text-[10px] uppercase tracking-[0.15em]">
                  {item.label}
                </span>
                {item.shortcut && (
                  <span className="ml-auto text-xs opacity-60">
                    {item.shortcut}
                  </span>
                )}
              </button>
            );
          })}
        </div>
      )}
    </div>
  );
}

Props

PropTypeDescription
triggerReactNodeThe element that triggers the menu on hover
items{ id: string; label: string; shortcut?: string }[]Menu items with optional keyboard shortcut labels
onSelect(id: string) => voidCallback when an item is selected