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.
"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
MenuBar
Prop | Type | Default |
---|---|---|
className? | string | undefined |
size? | "sm" | "default" | "lg" | "default" |
variant? | "default" | "outline" | "ghost" | "default" |
MenuBarTrigger
Prop | Type | Default |
---|---|---|
className? | string | undefined |
iconPosition? | "left" | "right" | "left" |
icon? | LucideIcon | undefined |
size? | "sm" | "default" | "lg" | "default" |
variant? | "default" | "ghost" | "default" |
MenuBarContent
Prop | Type | Default |
---|---|---|
className? | string | undefined |
sideOffset? | number | 8 |
align? | "start" | "center" | "end" | "start" |
variant? | "default" | "accent" | "default" |
MenuBarItem
Prop | Type | Default |
---|---|---|
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