shadimage

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