Kosorikōsori alpha

Menubar

A visually persistent menu common in desktop applications that provides quick access to a consistent set of commands.

Installation

Install the primitive

Install the @radix-ui/react-menubar package.

npm install @radix-ui/react-menubar

Copy-paste the component

Copy and paste the component code in a .tsx file.

import { forwardRef } from 'react';
import {
  CheckIcon,
  ChevronRightIcon,
  DotFilledIcon,
} from '@radix-ui/react-icons';
import {
  CheckboxItem,
  Content,
  Group,
  Item,
  ItemIndicator,
  Label,
  Menu,
  Portal,
  RadioGroup,
  RadioItem,
  Root,
  Separator,
  Sub,
  SubContent,
  SubTrigger,
  Trigger,
} from '@radix-ui/react-menubar';
import { clsx } from 'clsx/lite';
import { tv } from 'tailwind-variants';
 
const menubarStyles = tv({
  slots: {
    base: 'flex h-9 items-center space-x-1 rounded-lg border border-grey-line bg-grey-base p-1 shadow-sm',
    trigger: clsx(
      'flex h-full select-none items-center rounded-md px-3 py-1 text-sm font-medium outline-none transition-colors duration-200',
      'focus:bg-primary-bg-hover',
      'data-[state=open]:bg-primary-bg-hover',
    ),
    content: clsx(
      'z-50 min-w-[12rem] overflow-hidden rounded-lg border border-grey-line bg-grey-base p-1 shadow-md',
      'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
      'data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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',
    ),
    item: clsx(
      'group relative flex h-8 cursor-pointer select-none items-center rounded-md px-2 text-sm outline-none transition-colors duration-200',
      'focus:bg-primary-bg-hover',
      'active:bg-primary-bg-active',
      'data-[disabled]:cursor-not-allowed data-[disabled]:text-grey-solid',
    ),
    label:
      'flex h-8 select-none items-center px-2 text-xs font-medium text-grey-text',
    checkboxItem: clsx(
      'group relative flex h-8 cursor-pointer select-none items-center rounded-md pl-8 pr-2 text-sm outline-none transition-colors duration-200',
      'focus:bg-primary-bg-hover',
      'active:bg-primary-bg-active',
      'data-[disabled]:cursor-not-allowed data-[disabled]:text-grey-solid',
    ),
    checkboxItemIndicator: 'absolute left-2 flex items-center justify-center',
    checkboxItemIcon: 'size-4',
    radioItem: clsx(
      'relative flex h-8 cursor-pointer select-none items-center rounded-md pl-8 pr-2 text-sm outline-none transition-colors duration-200',
      'focus:bg-primary-bg-hover',
      'active:bg-primary-bg-active',
      'data-[disabled]:cursor-not-allowed data-[disabled]:text-grey-solid',
    ),
    radioItemIndicator: 'absolute left-2 flex items-center justify-center',
    radioItemIcon: 'size-4',
    separator: '-mx-1 my-1 h-px bg-grey-line',
    subTrigger: clsx(
      'group flex h-8 cursor-pointer select-none items-center rounded-md px-2 text-sm outline-none transition-colors duration-200',
      'focus:bg-primary-bg-hover',
      'data-[state=open]:bg-primary-bg-hover',
      'data-[disabled]:cursor-not-allowed data-[disabled]:text-grey-solid',
    ),
    subTriggerIcon: clsx(
      'ml-auto size-4',
      'group-data-[disabled]:text-grey-solid',
    ),
    subContent: clsx(
      'z-50 min-w-[8rem] overflow-hidden rounded-lg border border-grey-line bg-grey-base p-1 shadow-md',
      'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
      'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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',
    ),
    shortcut: clsx(
      'ml-auto text-xs tracking-widest text-grey-text',
      'group-data-[disabled]:text-grey-solid',
    ),
  },
  variants: {
    inset: {
      true: {
        item: 'pl-8',
        label: 'pl-8',
        subTrigger: 'pl-8',
      },
    },
  },
});
 
const {
  base,
  trigger,
  content,
  item,
  label,
  checkboxItem,
  checkboxItemIndicator,
  checkboxItemIcon,
  radioItem,
  radioItemIndicator,
  radioItemIcon,
  separator,
  subTrigger,
  subTriggerIcon,
  subContent,
  shortcut,
} = menubarStyles();
 
type MenubarRef = React.ElementRef<typeof Root>;
type MenubarProps = React.ComponentPropsWithoutRef<typeof Root>;
 
/**
 * Menubar component that serves as a container for the menu items.
 *
 * @param {MenubarProps} props - The props for the Menubar component.
 *
 * @example
 * <Menubar>
 *   <MenubarMenu>
 *     <MenubarTrigger>File</MenubarTrigger>
 *     <MenubarContent>
 *       <MenubarItem>
 *         New Tab <MenubarShortcut>⌘T</MenubarShortcut>
 *       </MenubarItem>
 *       <MenubarItem>New Window</MenubarItem>
 *       <MenubarSeparator />
 *       <MenubarItem>Share</MenubarItem>
 *       <MenubarSeparator />
 *       <MenubarItem>Print</MenubarItem>
 *     </MenubarContent>
 *   </MenubarMenu>
 * </Menubar>
 *
 * @see {@link https://dub.sh/ui-menubar Menubar Docs} for further information.
 */
export const Menubar = forwardRef<MenubarRef, MenubarProps>(
  ({ className, ...props }, ref) => (
    <Root ref={ref} className={base({ className })} {...props} />
  ),
);
 
Menubar.displayName = Root.displayName;
 
/**
 * MenubarMenu component that represents a menu within the menubar.
 *
 * @param {React.ComponentPropsWithoutRef<typeof Menu>} props - The props for the MenubarMenu component.
 *
 * @example
 * <MenubarMenu>
 *   <MenubarTrigger>File</MenubarTrigger>
 *   <MenubarContent>
 *     {Menu items here}
 *   </MenubarContent>
 * </MenubarMenu>
 */
export const MenubarMenu = Menu;
 
type MenubarTriggerRef = React.ElementRef<typeof Trigger>;
type MenubarTriggerProps = React.ComponentPropsWithoutRef<typeof Trigger>;
 
/**
 * MenubarTrigger component that triggers the display of the menu content.
 *
 * @param {MenubarTriggerProps} props - The props for the MenubarTrigger component.
 *
 * @example
 * <MenubarTrigger>File</MenubarTrigger>
 */
export const MenubarTrigger = forwardRef<
  MenubarTriggerRef,
  MenubarTriggerProps
>(({ className, ...props }, ref) => (
  <Trigger ref={ref} className={trigger({ className })} {...props} />
));
 
MenubarTrigger.displayName = Trigger.displayName;
 
/**
 * MenubarPortal component that renders the menu content in a portal.
 *
 * @param {React.ComponentPropsWithoutRef<typeof Portal>} props - The props for the MenubarPortal component.
 *
 * @example
 * <MenubarPortal>
 *   <MenubarContent>
 *     {Menu items here}
 *   <MenubarContent />
 * </MenubarPortal>
 */
export const MenubarPortal = Portal;
 
type MenubarContentRef = React.ElementRef<typeof Content>;
type MenubarContentProps = React.ComponentPropsWithoutRef<typeof Content>;
 
/**
 * MenubarContent component that displays the content of the menu.
 *
 * @param {MenubarContentProps} props - The props for the MenubarContent component.
 *
 * @example
 * <MenubarContent>
 *   {Menu items here}
 * </MenubarContent>
 */
export const MenubarContent = forwardRef<
  MenubarContentRef,
  MenubarContentProps
>(
  (
    { className, align = 'start', alignOffset = -4, sideOffset = 8, ...props },
    ref,
  ) => (
    <Portal>
      <Content
        ref={ref}
        align={align}
        alignOffset={alignOffset}
        className={content({ className })}
        sideOffset={sideOffset}
        {...props}
      />
    </Portal>
  ),
);
 
MenubarContent.displayName = Content.displayName;
 
type MenubarItemRef = React.ElementRef<typeof Item>;
type MenubarItemProps = React.ComponentPropsWithoutRef<typeof Item> & {
  inset?: boolean;
};
 
/**
 * MenubarItem component that represents an individual item in the menu.
 *
 * @param {MenubarItemProps} props - The props for the MenubarItem component.
 * @param {boolean} [inset] - Whether to add an inset to the item.
 *
 * @example
 * <MenubarItem>New Tab</MenubarItem>
 */
export const MenubarItem = forwardRef<MenubarItemRef, MenubarItemProps>(
  ({ className, inset, ...props }, ref) => (
    <Item ref={ref} className={item({ className, inset })} {...props} />
  ),
);
 
MenubarItem.displayName = Item.displayName;
 
/**
 * MenubarGroup component that groups related menu items.
 *
 * @param {React.ComponentPropsWithoutRef<typeof Group>} props - The props for the MenubarGroup component.
 *
 * @example
 * <MenubarGroup>
 *   {Grouped menu items here}
 * </MenubarGroup>
 */
export const MenubarGroup = Group;
 
type MenubarLabelRef = React.ElementRef<typeof Label>;
type MenubarLabelProps = React.ComponentPropsWithoutRef<typeof Label> & {
  inset?: boolean;
};
 
/**
 * MenubarLabel component that provides a label for a group of menu items.
 *
 * @param {MenubarLabelProps} props - The props for the MenubarLabel component.
 * @param {boolean} [inset] - Whether to add an inset to the label.
 *
 * @example
 * <MenubarLabel>File</MenubarLabel>
 */
export const MenubarLabel = forwardRef<MenubarLabelRef, MenubarLabelProps>(
  ({ className, inset, ...props }, ref) => (
    <Label ref={ref} className={label({ className, inset })} {...props} />
  ),
);
 
MenubarLabel.displayName = Label.displayName;
 
type MenubarCheckboxItemRef = React.ElementRef<typeof CheckboxItem>;
type MenubarCheckboxItemProps = React.ComponentPropsWithoutRef<
  typeof CheckboxItem
>;
 
/**
 * MenubarCheckboxItem component that represents a checkbox item in the menu.
 *
 * @param {MenubarCheckboxItemProps} props - The props for the MenubarCheckboxItem component.
 *
 * @example
 * <MenubarCheckboxItem>Enable Feature</MenubarCheckboxItem>
 */
export const MenubarCheckboxItem = forwardRef<
  MenubarCheckboxItemRef,
  MenubarCheckboxItemProps
>(({ className, children, checked, ...props }, ref) => (
  <CheckboxItem
    ref={ref}
    checked={checked}
    className={checkboxItem({ className })}
    {...props}
  >
    <span className={checkboxItemIndicator()}>
      <ItemIndicator>
        <CheckIcon className={checkboxItemIcon()} />
      </ItemIndicator>
    </span>
    {children}
  </CheckboxItem>
));
 
MenubarCheckboxItem.displayName = CheckboxItem.displayName;
 
/**
 * MenubarRadioGroup component that groups radio items in the menu.
 *
 * @param {React.ComponentPropsWithoutRef<typeof RadioGroup>} props - The props for the MenubarRadioGroup component.
 *
 * @example
 * <MenubarRadioGroup>
 *   {Radio items here}
 * </MenubarRadioGroup>
 */
export const MenubarRadioGroup = RadioGroup;
 
type MenubarRadioItemRef = React.ElementRef<typeof RadioItem>;
type MenubarRadioItemProps = React.ComponentPropsWithoutRef<typeof RadioItem>;
 
/**
 * MenubarRadioItem component that represents a radio item in the menu.
 *
 * @param {MenubarRadioItemProps} props - The props for the MenubarRadioItem component.
 *
 * @example
 * <MenubarRadioItem>Option 1</MenubarRadioItem>
 */
export const MenubarRadioItem = forwardRef<
  MenubarRadioItemRef,
  MenubarRadioItemProps
>(({ className, children, ...props }, ref) => (
  <RadioItem ref={ref} className={radioItem({ className })} {...props}>
    <span className={radioItemIndicator()}>
      <ItemIndicator>
        <DotFilledIcon className={radioItemIcon()} />
      </ItemIndicator>
    </span>
    {children}
  </RadioItem>
));
 
MenubarRadioItem.displayName = RadioItem.displayName;
 
type MenubarSeparatorRef = React.ElementRef<typeof Separator>;
type MenubarSeparatorProps = React.ComponentPropsWithoutRef<typeof Separator>;
 
/**
 * MenubarSeparator component that visually separates menu items.
 *
 * @param {MenubarSeparatorProps} props - The props for the MenubarSeparator component.
 *
 * @example
 * <MenubarSeparator />
 */
export const MenubarSeparator = forwardRef<
  MenubarSeparatorRef,
  MenubarSeparatorProps
>(({ className, ...props }, ref) => (
  <Separator ref={ref} className={separator({ className })} {...props} />
));
 
MenubarSeparator.displayName = Separator.displayName;
 
/**
 * MenubarSub component that represents a submenu in the menubar.
 *
 * @param {React.ComponentPropsWithoutRef<typeof Sub>} props - The props for the MenubarSub component.
 *
 * @example
 * <MenubarSub>
 *   {Submenu items here}
 * </MenubarSub>
 */
export const MenubarSub = Sub;
 
type MenubarSubTriggerRef = React.ElementRef<typeof SubTrigger>;
type MenubarSubTriggerProps = React.ComponentPropsWithoutRef<
  typeof SubTrigger
> & {
  inset?: boolean;
};
 
/**
 * MenubarSubTrigger component that triggers the display of a submenu.
 *
 * @param {MenubarSubTriggerProps} props - The props for the MenubarSubTrigger component.
 *
 * @example
 * <MenubarSubTrigger>More Options</MenubarSubTrigger>
 */
export const MenubarSubTrigger = forwardRef<
  MenubarSubTriggerRef,
  MenubarSubTriggerProps
>(({ className, inset, children, ...props }, ref) => (
  <SubTrigger ref={ref} className={subTrigger({ className, inset })} {...props}>
    {children}
    <ChevronRightIcon className={subTriggerIcon()} />
  </SubTrigger>
));
 
MenubarSubTrigger.displayName = SubTrigger.displayName;
 
type MenubarSubContentRef = React.ElementRef<typeof SubContent>;
type MenubarSubContentProps = React.ComponentPropsWithoutRef<typeof SubContent>;
 
/**
 * MenubarSubContent component that displays the content of a submenu.
 *
 * @param {MenubarSubContentProps} props - The props for the MenubarSubContent component.
 *
 * @example
 * <MenubarSubContent>
 *   {Submenu items here}
 * </MenubarSubContent>
 */
export const MenubarSubContent = forwardRef<
  MenubarSubContentRef,
  MenubarSubContentProps
>(({ className, ...props }, ref) => (
  <SubContent ref={ref} className={subContent({ className })} {...props} />
));
 
MenubarSubContent.displayName = SubContent.displayName;
 
type MenubarShortcutProps = React.HTMLAttributes<HTMLSpanElement>;
 
/**
 * MenubarShortcut component that displays a shortcut key for a menu item.
 *
 * @param {MenubarShortcutProps} props - The props for the MenubarShortcut component.
 *
 * @example
 * <MenubarShortcut>⌘T</MenubarShortcut>
 */
export const MenubarShortcut = ({
  className,
  ...props
}: MenubarShortcutProps) => (
  <span className={shortcut({ className })} {...props} />
);
 
MenubarShortcut.displayName = 'MenubarShortcut';

Update import paths

Update the @kosori/ui import paths to fit your project structure, for example, using ~/components/ui.

Usage

import {
  Menubar,
  MenubarContent,
  MenubarItem,
  MenubarMenu,
  MenubarSeparator,
  MenubarShortcut,
  MenubarTrigger,
} from '~/components/ui/menubar';
<Menubar>
  <MenubarMenu>
    <MenubarTrigger>File</MenubarTrigger>
    <MenubarContent>
      <MenubarItem>
        New Tab <MenubarShortcut>⌘T</MenubarShortcut>
      </MenubarItem>
      <MenubarItem>New Window</MenubarItem>
      <MenubarSeparator />
      <MenubarItem>Share</MenubarItem>
      <MenubarSeparator />
      <MenubarItem>Print</MenubarItem>
    </MenubarContent>
  </MenubarMenu>
</Menubar>

On this page