Menubar
A horizontal menu bar with dropdown menus, commonly used for application navigation.
ui/menubar.tsx
"use client";
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
import { Menubar as MenubarPrimitive } from "@base-ui/react/menubar";
import { CheckIcon, ChevronRightIcon } from "blode-icons-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Menubar = ({ className, ...props }: MenubarPrimitive.Props) => (
<MenubarPrimitive
className={cn(
"flex h-8 items-center gap-0.5 rounded-lg border bg-background p-[3px]",
className,
)}
data-slot="menubar"
{...props}
/>
);
const MenubarMenu = ({ ...props }: MenuPrimitive.Root.Props) => (
<MenuPrimitive.Root data-slot="menubar-menu" {...props} />
);
const MenubarGroup = ({ ...props }: MenuPrimitive.Group.Props) => (
<MenuPrimitive.Group data-slot="menubar-group" {...props} />
);
const MenubarPortal = ({ ...props }: MenuPrimitive.Portal.Props) => (
<MenuPrimitive.Portal data-slot="menubar-portal" {...props} />
);
const MenubarRadioGroup = ({ ...props }: MenuPrimitive.RadioGroup.Props) => (
<MenuPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
);
const MenubarTrigger = ({
className,
asChild,
children,
...props
}: MenuPrimitive.Trigger.Props & {
asChild?: boolean;
}) => {
if (asChild && React.isValidElement(children)) {
return (
<MenuPrimitive.Trigger
className={cn(
"flex select-none items-center rounded-sm px-1.5 py-[2px] font-medium text-sm outline-hidden hover:bg-muted aria-expanded:bg-muted data-popup-open:bg-muted",
className,
)}
data-slot="menubar-trigger"
render={children}
{...props}
/>
);
}
return (
<MenuPrimitive.Trigger
className={cn(
"flex select-none items-center rounded-sm px-1.5 py-[2px] font-medium text-sm outline-hidden hover:bg-muted aria-expanded:bg-muted data-popup-open:bg-muted",
className,
)}
data-slot="menubar-trigger"
{...props}
>
{children}
</MenuPrimitive.Trigger>
);
};
const MenubarContent = ({
className,
align = "start",
alignOffset = -4,
sideOffset = 8,
...props
}: MenuPrimitive.Popup.Props &
Pick<MenuPrimitive.Positioner.Props, "align" | "alignOffset" | "sideOffset">) => (
<MenubarPortal>
<MenuPrimitive.Positioner
align={align}
alignOffset={alignOffset}
className="isolate z-50 outline-none"
sideOffset={sideOffset}
>
<MenuPrimitive.Popup
className={cn(
"data-open:fade-in-0 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 cn-menu-target z-50 min-w-36 origin-(--transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-open:animate-in",
className,
)}
data-slot="menubar-content"
{...props}
/>
</MenuPrimitive.Positioner>
</MenubarPortal>
);
const MenubarItem = ({
className,
inset,
variant = "default",
asChild,
children,
...props
}: MenuPrimitive.Item.Props & {
inset?: boolean;
variant?: "default" | "destructive";
asChild?: boolean;
}) => {
if (asChild && React.isValidElement(children)) {
return (
<MenuPrimitive.Item
className={cn(
"group/menubar-item relative flex cursor-default select-none items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7 data-[variant=destructive]:text-destructive data-disabled:opacity-50 data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 data-[variant=destructive]:*:[svg]:text-destructive!",
className,
)}
data-inset={inset}
data-slot="menubar-item"
data-variant={variant}
render={children}
{...props}
/>
);
}
return (
<MenuPrimitive.Item
className={cn(
"group/menubar-item relative flex cursor-default select-none items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7 data-[variant=destructive]:text-destructive data-disabled:opacity-50 data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 data-[variant=destructive]:*:[svg]:text-destructive!",
className,
)}
data-inset={inset}
data-slot="menubar-item"
data-variant={variant}
{...props}
>
{children}
</MenuPrimitive.Item>
);
};
type MenubarCheckedState = boolean | "indeterminate";
type MenubarCheckboxItemProps = Omit<
MenuPrimitive.CheckboxItem.Props,
"checked" | "defaultChecked" | "onCheckedChange"
> & {
checked?: MenubarCheckedState;
defaultChecked?: MenubarCheckedState;
onCheckedChange?: (
checked: MenubarCheckedState,
eventDetails: MenuPrimitive.CheckboxItem.ChangeEventDetails,
) => void;
inset?: boolean;
};
const MenubarCheckboxItem = ({
className,
children,
checked,
defaultChecked,
onCheckedChange,
inset,
...props
}: MenubarCheckboxItemProps) => (
<MenuPrimitive.CheckboxItem
checked={checked === "indeterminate" ? true : checked}
className={cn(
"relative flex cursor-default select-none items-center gap-1.5 rounded-md py-1 pr-1.5 pl-7 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
data-inset={inset}
data-slot="menubar-checkbox-item"
defaultChecked={defaultChecked === "indeterminate" ? true : defaultChecked}
onCheckedChange={(value, eventDetails) => onCheckedChange?.(value, eventDetails)}
{...props}
>
<span className="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4">
<MenuPrimitive.CheckboxItemIndicator>
<CheckIcon />
</MenuPrimitive.CheckboxItemIndicator>
</span>
{children}
</MenuPrimitive.CheckboxItem>
);
const MenubarRadioItem = ({
className,
children,
inset,
...props
}: MenuPrimitive.RadioItem.Props & {
inset?: boolean;
}) => (
<MenuPrimitive.RadioItem
className={cn(
"relative flex cursor-default select-none items-center gap-1.5 rounded-md py-1 pr-1.5 pl-7 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7 data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
data-inset={inset}
data-slot="menubar-radio-item"
{...props}
>
<span className="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4">
<MenuPrimitive.RadioItemIndicator>
<CheckIcon />
</MenuPrimitive.RadioItemIndicator>
</span>
{children}
</MenuPrimitive.RadioItem>
);
const MenubarLabel = ({
className,
inset,
...props
}: MenuPrimitive.GroupLabel.Props & {
inset?: boolean;
}) => (
<MenuPrimitive.GroupLabel
className={cn("px-1.5 py-1 font-medium text-sm data-inset:pl-7", className)}
data-inset={inset}
data-slot="menubar-label"
{...props}
/>
);
const MenubarSeparator = ({ className, ...props }: MenuPrimitive.Separator.Props) => (
<MenuPrimitive.Separator
className={cn("-mx-1 my-1 h-px bg-border", className)}
data-slot="menubar-separator"
{...props}
/>
);
const MenubarShortcut = ({ className, ...props }: React.ComponentProps<"span">) => (
<span
className={cn(
"ml-auto text-muted-foreground text-xs tracking-widest group-focus/menubar-item:text-accent-foreground",
className,
)}
data-slot="menubar-shortcut"
{...props}
/>
);
const MenubarSub = ({ ...props }: MenuPrimitive.SubmenuRoot.Props) => (
<MenuPrimitive.SubmenuRoot data-slot="menubar-sub" {...props} />
);
const MenubarSubTrigger = ({
className,
inset,
children,
...props
}: MenuPrimitive.SubmenuTrigger.Props & {
inset?: boolean;
}) => (
<MenuPrimitive.SubmenuTrigger
className={cn(
"flex cursor-default select-none items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-popup-open:bg-accent data-inset:pl-7 data-popup-open:text-accent-foreground [&_svg:not([class*='size-'])]:size-4",
className,
)}
data-inset={inset}
data-slot="menubar-sub-trigger"
{...props}
>
{children}
<ChevronRightIcon className="cn-rtl-flip ml-auto size-4" />
</MenuPrimitive.SubmenuTrigger>
);
const MenubarSubContent = ({
className,
align = "start",
alignOffset = -3,
side = "right",
sideOffset = 0,
...props
}: MenuPrimitive.Popup.Props &
Pick<MenuPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) => (
<MenuPrimitive.Portal>
<MenuPrimitive.Positioner
align={align}
alignOffset={alignOffset}
className="isolate z-50 outline-none"
side={side}
sideOffset={sideOffset}
>
<MenuPrimitive.Popup
className={cn(
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 cn-menu-target z-50 min-w-32 origin-(--transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-closed:animate-out data-open:animate-in",
className,
)}
data-slot="menubar-sub-content"
{...props}
/>
</MenuPrimitive.Positioner>
</MenuPrimitive.Portal>
);
export {
Menubar,
MenubarPortal,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarGroup,
MenubarSeparator,
MenubarLabel,
MenubarItem,
MenubarShortcut,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSub,
MenubarSubTrigger,
MenubarSubContent,
};
- Interaction
- dropdown
DependenciesBundle weight · light
- ui-primitives
- @base-ui/react