Build websites 10x faster with HextaUI Blocks — Learn more
UI/UI

Menubar

A horizontal menu bar component for navigation and actions.

import {
  MenuBar,
  MenuBarMenu,
  MenuBarTrigger,
  MenuBarContent,
  MenuBarItem,
  MenuBarSeparator,
} from "@/components/ui/MenuBar";
import { File, Edit, View, Settings, HelpCircle } from "lucide-react";

<MenuBar>
  <MenuBarMenu>
    <MenuBarTrigger icon={File}>File</MenuBarTrigger>
    <MenuBarContent>
      <MenuBarItem icon={Plus} shortcut="⌘N">
        New File
      </MenuBarItem>
      <MenuBarItem icon={Save} shortcut="⌘S">
        Save
      </MenuBarItem>
    </MenuBarContent>
  </MenuBarMenu>
  <MenuBarMenu>
    <MenuBarTrigger icon={Edit}>Edit</MenuBarTrigger>
    <MenuBarContent>
      <MenuBarItem shortcut="⌘Z">Undo</MenuBarItem>
      <MenuBarItem shortcut="⌘Y">Redo</MenuBarItem>
    </MenuBarContent>
  </MenuBarMenu>
</MenuBar>

Installation

Install following dependencies:

npm install @radix-ui/react-menubar class-variance-authority lucide-react motion
pnpm add @radix-ui/react-menubar class-variance-authority lucide-react motion
yarn add @radix-ui/react-menubar class-variance-authority lucide-react motion
bun add @radix-ui/react-menubar class-variance-authority lucide-react motion

Copy and paste the following code into your project.

components/ui/menubar.tsx
"use client";

import * as React from "react";
import * as MenubarPrimitive from "@radix-ui/react-menubar";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { motion, AnimatePresence } from "motion/react";
import { type LucideIcon } from "lucide-react";

const menubarVariants = cva(
  "flex items-center rounded-2xl border border-[hsl(var(--hu-border))] bg-[hsl(var(--hu-background))] transition-all overflow-x-auto scrollbar-hide w-full max-w-full",
  {
    variants: {
      variant: {
        default:
          "bg-[hsl(var(--hu-background))] border-[hsl(var(--hu-border))]",
        outline:
          "border-2 border-[hsl(var(--hu-border))] bg-[hsl(var(--hu-background))]",
        ghost: "border-transparent bg-transparent shadow-none",
      },
      size: {
        sm: "p-1.5 gap-1 sm:p-2 sm:gap-1",
        default: "p-1.5 gap-1.5 sm:p-2 sm:gap-2",
        lg: "p-2 gap-2 sm:p-2 sm:gap-3",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  },
);

const menubarTriggerVariants = cva(
  "flex cursor-default select-none items-center rounded-[var(--radius)] font-medium outline-none transition-all touch-manipulation focus-visible:ring-2 focus-visible:ring-[hsl(var(--hu-ring))] focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 whitespace-nowrap",
  {
    variants: {
      variant: {
        default:
          "text-[hsl(var(--hu-foreground))] hover:bg-[hsl(var(--hu-accent))] hover:text-[hsl(var(--hu-accent-foreground))] focus:bg-[hsl(var(--hu-accent))] focus:text-[hsl(var(--hu-accent-foreground))] data-[state=open]:bg-[hsl(var(--hu-accent))] data-[state=open]:text-[hsl(var(--hu-accent-foreground))] active:bg-[hsl(var(--hu-accent))] active:text-[hsl(var(--hu-accent-foreground))]",
        ghost:
          "text-[hsl(var(--hu-foreground))] hover:bg-[hsl(var(--hu-accent))]/50 hover:text-[hsl(var(--hu-accent-foreground))] focus:bg-[hsl(var(--hu-accent))]/50 focus:text-[hsl(var(--hu-accent-foreground))] data-[state=open]:bg-[hsl(var(--hu-accent))]/50 data-[state=open]:text-[hsl(var(--hu-accent-foreground))]",
      },
      size: {
        sm: "px-2.5 py-1.5 text-xs gap-1 min-h-[2rem] [&_svg]:size-3 sm:px-3 sm:py-2 sm:text-sm sm:gap-1.5 sm:[&_svg]:size-4",
        default:
          "px-3 py-2 text-sm gap-1.5 min-h-[2.5rem] [&_svg]:size-4 sm:px-4 sm:py-2.5 sm:gap-2",
        lg: "px-4 py-2.5 text-sm gap-2 min-h-[3rem] [&_svg]:size-4 sm:px-5 sm:py-3 sm:text-base sm:gap-2.5 sm:[&_svg]:size-5",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  },
);

const menubarContentVariants = cva(
  "z-50 min-w-[12rem] max-w-[95vw] sm:max-w-[350px] overflow-hidden rounded-[var(--radius)] border border-[hsl(var(--hu-border))] bg-[hsl(var(--hu-background))] p-1.5 sm:p-2 text-[hsl(var(--hu-foreground))] shadow-xl  mt-2",
  {
    variants: {
      variant: {
        default:
          "bg-[hsl(var(--hu-background))] border-[hsl(var(--hu-border))]",
        accent:
          "bg-[hsl(var(--hu-accent))] text-[hsl(var(--hu-accent-foreground))] border-[hsl(var(--hu-accent))]",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  },
);

const menubarItemVariants = cva(
  "relative flex cursor-default select-none items-center gap-2 rounded-[var(--radius)] px-2.5 py-2 sm:px-3 sm:py-2.5 text-sm outline-none transition-all touch-manipulation focus-visible:ring-2 focus-visible:ring-[hsl(var(--hu-ring))] focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 min-h-[44px] sm:min-h-auto",
  {
    variants: {
      variant: {
        default:
          "text-[hsl(var(--hu-foreground))] hover:bg-[hsl(var(--hu-accent))] hover:text-[hsl(var(--hu-accent-foreground))] focus:bg-[hsl(var(--hu-accent))] focus:text-[hsl(var(--hu-accent-foreground))] active:bg-[hsl(var(--hu-accent))] active:text-[hsl(var(--hu-accent-foreground))]",
        destructive:
          "text-[hsl(var(--hu-destructive))] hover:bg-[hsl(var(--hu-destructive))] hover:text-[hsl(var(--hu-destructive-foreground))] focus:bg-[hsl(var(--hu-destructive))] focus:text-[hsl(var(--hu-destructive-foreground))] active:bg-[hsl(var(--hu-destructive))] active:text-[hsl(var(--hu-destructive-foreground))]",
      },
      inset: {
        true: "pl-6 sm:pl-8",
        false: "",
      },
    },
    defaultVariants: {
      variant: "default",
      inset: false,
    },
  },
);

interface MenuBarProps
  extends React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>,
    VariantProps<typeof menubarVariants> {
  /**
   * Enable mobile-responsive mode
   * @default false
   */
  responsive?: boolean;
}

interface MenuBarTriggerProps
  extends React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>,
    VariantProps<typeof menubarTriggerVariants> {
  icon?: LucideIcon;
  iconPosition?: "left" | "right";
}

interface MenuBarContentProps
  extends React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>,
    VariantProps<typeof menubarContentVariants> {}

interface MenuBarItemProps
  extends React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item>,
    VariantProps<typeof menubarItemVariants> {
  icon?: LucideIcon;
  shortcut?: string;
}

const MenuBar = React.forwardRef<
  React.ElementRef<typeof MenubarPrimitive.Root>,
  MenuBarProps
>(({ className, variant, size, responsive = false, ...props }, ref) => (
  <div className={responsive ? "w-full overflow-x-auto scrollbar-hide" : ""}>
    <MenubarPrimitive.Root
      ref={ref}
      className={cn(
        menubarVariants({ variant, size }),
        responsive && "min-w-max",
        className,
      )}
      {...props}
    />
  </div>
));
MenuBar.displayName = "MenuBar";

const MenuBarMenu = MenubarPrimitive.Menu;

const MenuBarTrigger = React.forwardRef<
  React.ElementRef<typeof MenubarPrimitive.Trigger>,
  MenuBarTriggerProps
>(
  (
    {
      className,
      variant,
      size,
      icon: Icon,
      iconPosition = "left",
      children,
      ...props
    },
    ref,
  ) => {
    // Responsive icon sizes
    const iconSize = size === "sm" ? 14 : size === "lg" ? 18 : 16;
    const mobileIconSize = size === "sm" ? 12 : size === "lg" ? 16 : 14;

    return (
      <MenubarPrimitive.Trigger
        ref={ref}
        className={cn(menubarTriggerVariants({ variant, size }), className)}
        asChild
        {...props}
      >
        <motion.button
          whileHover={{ scale: 1.02 }}
          whileTap={{ scale: 0.98 }}
          transition={{
            type: "spring",
            stiffness: 300,
            damping: 20,
            duration: 0.1,
          }}
          className="flex items-center gap-1.5 sm:gap-2"
        >
          {Icon && iconPosition === "left" && (
            <motion.div
              whileHover={{ rotate: 5 }}
              transition={{ duration: 0.15 }}
            >
              <Icon size={iconSize} className="shrink-0 hidden sm:block" />
              <Icon
                size={mobileIconSize}
                className="shrink-0 block sm:hidden"
              />
            </motion.div>
          )}
          <span className="truncate">{children}</span>
          {Icon && iconPosition === "right" && (
            <motion.div
              whileHover={{ rotate: -5 }}
              transition={{ duration: 0.15 }}
            >
              <Icon size={iconSize} className="shrink-0 hidden sm:block" />
              <Icon
                size={mobileIconSize}
                className="shrink-0 block sm:hidden"
              />
            </motion.div>
          )}
        </motion.button>
      </MenubarPrimitive.Trigger>
    );
  },
);
MenuBarTrigger.displayName = "MenuBarTrigger";

const MenuBarSub = MenubarPrimitive.Sub;

const MenuBarSubTrigger = React.forwardRef<
  React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
    inset?: boolean;
    icon?: LucideIcon;
  }
>(({ className, inset, icon: Icon, children, ...props }, ref) => (
  <MenubarPrimitive.SubTrigger
    ref={ref}
    className={cn(
      "flex cursor-default select-none items-center gap-2 rounded-[var(--radius)] px-2.5 py-2 sm:px-3 sm:py-2.5 text-sm outline-none transition-all touch-manipulation focus-visible:ring-2 focus-visible:ring-[hsl(var(--hu-ring))] focus-visible:ring-offset-2 hover:bg-[hsl(var(--hu-accent))] hover:text-[hsl(var(--hu-accent-foreground))] focus:bg-[hsl(var(--hu-accent))] focus:text-[hsl(var(--hu-accent-foreground))] data-[state=open]:bg-[hsl(var(--hu-accent))] data-[state=open]:text-[hsl(var(--hu-accent-foreground))] active:bg-[hsl(var(--hu-accent))] active:text-[hsl(var(--hu-accent-foreground))] [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 min-h-[44px] sm:min-h-auto",
      inset && "pl-6 sm:pl-8",
      className,
    )}
    {...props}
  >
    <motion.div
      className="flex text-sm w-full items-center gap-1.5 sm:gap-2"
      whileHover={{ x: 2 }}
      transition={{ duration: 0.1 }}
    >
      {Icon && (
        <>
          <Icon size={14} className="shrink-0 block sm:hidden" />
          <Icon size={16} className="shrink-0 hidden sm:block" />
        </>
      )}
      <span className="flex-1 truncate">{children}</span>
      <svg
        className="ml-auto h-3.5 w-3.5 sm:h-4 sm:w-4"
        fill="none"
        viewBox="0 0 24 24"
        stroke="currentColor"
      >
        <path
          strokeLinecap="round"
          strokeLinejoin="round"
          strokeWidth={2}
          d="M9 5l7 7-7 7"
        />
      </svg>
    </motion.div>
  </MenubarPrimitive.SubTrigger>
));
MenuBarSubTrigger.displayName = "MenuBarSubTrigger";

const MenuBarSubContent = React.forwardRef<
  React.ElementRef<typeof MenubarPrimitive.SubContent>,
  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
  <MenubarPrimitive.SubContent
    ref={ref}
    className={cn(
      "z-50 min-w-[10rem] max-w-[95vw] sm:max-w-[280px] overflow-hidden rounded-[var(--radius)] border border-[hsl(var(--hu-border))] bg-[hsl(var(--hu-background))] p-1.5 sm:p-2 text-[hsl(var(--hu-foreground))] shadow-xl ",
      className,
    )}
    asChild
    {...props}
  >
    <motion.div
      initial={{
        opacity: 0,
        scale: 0.95,
        x: -8,
      }}
      animate={{
        opacity: 1,
        scale: 1,
        x: 0,
      }}
      exit={{
        opacity: 0,
        scale: 0.95,
        x: -8,
      }}
      transition={{
        type: "spring",
        stiffness: 400,
        damping: 30,
        mass: 0.8,
        duration: 0.15,
      }}
    >
      {props.children}
    </motion.div>
  </MenubarPrimitive.SubContent>
));
MenuBarSubContent.displayName = "MenuBarSubContent";

const MenuBarContent = React.forwardRef<
  React.ElementRef<typeof MenubarPrimitive.Content>,
  MenuBarContentProps
>(
  (
    {
      className,
      variant,
      align = "start",
      alignOffset = -4,
      sideOffset = 8,
      ...props
    },
    ref,
  ) => (
    <AnimatePresence>
      <MenubarPrimitive.Portal>
        <MenubarPrimitive.Content
          ref={ref}
          align={align}
          alignOffset={alignOffset}
          sideOffset={sideOffset}
          className={cn(menubarContentVariants({ variant }), className)}
          asChild
          {...props}
        >
          <motion.div
            initial={{
              opacity: 0,
              scale: 0.95,
              y: -8,
            }}
            animate={{
              opacity: 1,
              scale: 1,
              y: 0,
            }}
            exit={{
              opacity: 0,
              scale: 0.95,
              y: -8,
            }}
            transition={{
              type: "spring",
              stiffness: 400,
              damping: 30,
              mass: 0.8,
              duration: 0.2,
            }}
          >
            {props.children}
          </motion.div>
        </MenubarPrimitive.Content>
      </MenubarPrimitive.Portal>
    </AnimatePresence>
  ),
);
MenuBarContent.displayName = "MenuBarContent";

const MenuBarItem = React.forwardRef<
  React.ElementRef<typeof MenubarPrimitive.Item>,
  MenuBarItemProps
>(
  (
    { className, variant, inset, icon: Icon, shortcut, children, ...props },
    ref,
  ) => (
    <MenubarPrimitive.Item
      ref={ref}
      className={cn(menubarItemVariants({ variant, inset }), className)}
      asChild
      {...props}
    >
      <motion.div
        whileHover={{
          scale: 1.02,
          x: 2,
        }}
        whileTap={{
          scale: 0.98,
        }}
        transition={{
          type: "spring",
          stiffness: 300,
          damping: 20,
          duration: 0.1,
        }}
        className="flex items-center gap-1.5 sm:gap-2 w-full"
      >
        {Icon && (
          <>
            <Icon size={14} className="shrink-0 block sm:hidden" />
            <Icon size={16} className="shrink-0 hidden sm:block" />
          </>
        )}
        <span className="flex-1 truncate">{children}</span>
        {shortcut && (
          <motion.span
            className="ml-auto text-xs tracking-widest text-[hsl(var(--hu-muted-foreground))] font-mono hidden sm:inline"
            initial={{ opacity: 0.6 }}
            whileHover={{ opacity: 1 }}
            transition={{ duration: 0.15 }}
          >
            {shortcut}
          </motion.span>
        )}
      </motion.div>
    </MenubarPrimitive.Item>
  ),
);
MenuBarItem.displayName = "MenuBarItem";

const MenuBarSeparator = React.forwardRef<
  React.ElementRef<typeof MenubarPrimitive.Separator>,
  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
  <MenubarPrimitive.Separator
    ref={ref}
    className={cn("my-2 h-px bg-[hsl(var(--hu-border))] mx-1", className)}
    {...props}
  />
));
MenuBarSeparator.displayName = "MenuBarSeparator";

// Animation variants for staggered menu items
const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.05,
      delayChildren: 0.1,
    },
  },
};

const itemVariants = {
  hidden: {
    opacity: 0,
    x: -10,
    scale: 0.95,
  },
  visible: {
    opacity: 1,
    x: 0,
    scale: 1,
    transition: {
      type: "spring",
      stiffness: 300,
      damping: 20,
    },
  },
};

// Animated container for menu items
const AnimatedMenuContainer = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ children, className, ...props }, ref) => (
  <motion.div
    ref={ref}
    className={className}
    variants={containerVariants}
    initial="hidden"
    animate="visible"
    style={props.style}
  >
    {React.Children.map(children, (child, index) => (
      <motion.div key={index} variants={itemVariants}>
        {child}
      </motion.div>
    ))}
  </motion.div>
));
AnimatedMenuContainer.displayName = "AnimatedMenuContainer";

export {
  MenuBar,
  MenuBarMenu,
  MenuBarTrigger,
  MenuBarContent,
  MenuBarItem,
  MenuBarSeparator,
  MenuBarSub,
  MenuBarSubTrigger,
  MenuBarSubContent,
  menubarVariants,
  menubarTriggerVariants,
  menubarContentVariants,
  menubarItemVariants,
  AnimatedMenuContainer,
};
npx hextaui@latest add menubar
pnpm dlx hextaui@latest add menubar
yarn dlx hextaui@latest add menubar
bun x hextaui@latest add menubar

Usage

import {
  MenuBar,
  MenuBarMenu,
  MenuBarTrigger,
  MenuBarContent,
  MenuBarItem,
  MenuBarSeparator,
} from "@/components/ui/MenuBar";
<MenuBar>
  <MenuBarMenu>
    <MenuBarTrigger>File</MenuBarTrigger>
    <MenuBarContent>
      <MenuBarItem>New File</MenuBarItem>
      <MenuBarItem>Open File</MenuBarItem>
      <MenuBarSeparator />
      <MenuBarItem>Save</MenuBarItem>
    </MenuBarContent>
  </MenuBarMenu>
</MenuBar>

Examples

Basic Usage

import {
  MenuBar,
  MenuBarMenu,
  MenuBarTrigger,
  MenuBarContent,
  MenuBarItem,
  MenuBarSeparator,
  MenuBarSub,
  MenuBarSubTrigger,
  MenuBarSubContent,
} from "@/components/ui/MenuBar";
import { File, Edit, Settings, Plus, Save } from "lucide-react";

<MenuBar>
  <MenuBarMenu>
    <MenuBarTrigger icon={File}>File</MenuBarTrigger>
    <MenuBarContent>
      <MenuBarItem icon={Plus} shortcut="⌘N">
        New File
      </MenuBarItem>
      <MenuBarItem icon={Save} shortcut="⌘S">
        Save
      </MenuBarItem>
    </MenuBarContent>
  </MenuBarMenu>

  <MenuBarMenu>
    <MenuBarTrigger icon={Edit}>Edit</MenuBarTrigger>
    <MenuBarContent>
      <MenuBarItem shortcut="⌘Z">Undo</MenuBarItem>
      <MenuBarItem shortcut="⌘Y">Redo</MenuBarItem>
    </MenuBarContent>
  </MenuBarMenu>

  <MenuBarMenu>
    <MenuBarTrigger icon={Settings}>Settings</MenuBarTrigger>
    <MenuBarContent>
      <MenuBarSub>
        <MenuBarSubTrigger>Preferences</MenuBarSubTrigger>
        <MenuBarSubContent>
          <MenuBarItem>Theme</MenuBarItem>
          <MenuBarItem>Language</MenuBarItem>
        </MenuBarSubContent>
      </MenuBarSub>
    </MenuBarContent>
  </MenuBarMenu>
</MenuBar>

Simple Example

import {
  MenuBar,
  MenuBarMenu,
  MenuBarTrigger,
  MenuBarContent,
  MenuBarItem,
  MenuBarSeparator,
} from "@/components/ui/MenuBar";

<MenuBar>
  <MenuBarMenu>
    <MenuBarTrigger>File</MenuBarTrigger>
    <MenuBarContent>
      <MenuBarItem>New</MenuBarItem>
      <MenuBarItem>Open</MenuBarItem>
      <MenuBarItem>Save</MenuBarItem>
      <MenuBarSeparator />
      <MenuBarItem>Exit</MenuBarItem>
    </MenuBarContent>
  </MenuBarMenu>

  <MenuBarMenu>
    <MenuBarTrigger>Edit</MenuBarTrigger>
    <MenuBarContent>
      <MenuBarItem>Undo</MenuBarItem>
      <MenuBarItem>Redo</MenuBarItem>
      <MenuBarSeparator />
      <MenuBarItem>Cut</MenuBarItem>
      <MenuBarItem>Copy</MenuBarItem>
      <MenuBarItem>Paste</MenuBarItem>
    </MenuBarContent>
  </MenuBarMenu>

  <MenuBarMenu>
    <MenuBarTrigger>Help</MenuBarTrigger>
    <MenuBarContent>
      <MenuBarItem>Documentation</MenuBarItem>
      <MenuBarItem>Support</MenuBarItem>
      <MenuBarSeparator />
      <MenuBarItem>About</MenuBarItem>
    </MenuBarContent>
  </MenuBarMenu>
</MenuBar>

Props

PropTypeDefault
className?
string
undefined
size?
"sm" | "default" | "lg"
"default"
variant?
"default" | "outline" | "ghost"
"default"
PropTypeDefault
className?
string
undefined
iconPosition?
"left" | "right"
"left"
icon?
LucideIcon
undefined
size?
"sm" | "default" | "lg"
"default"
variant?
"default" | "ghost"
"default"
PropTypeDefault
className?
string
undefined
sideOffset?
number
8
align?
"start" | "center" | "end"
"start"
variant?
"default" | "accent"
"default"
PropTypeDefault
disabled?
boolean
false
className?
string
undefined
shortcut?
string
undefined
icon?
LucideIcon
undefined
inset?
boolean
false
variant?
"default" | "destructive"
"default"
Edit on GitHub

Last updated on