Glrk UI

Menubar

A wrapper for Base UI Menubar — multiple dropdown menus in a horizontal bar with plain, checkbox, and radio variants.

Default
Multiple menus
Checkbox
Indicator right (default)
Indicator left
With group labels
Radio
Controlled
Indicator left
With group labels

Installation

npx shadcn@latest add @glrk-ui/menubar

If you haven't set up the prerequisites yet, check out Prerequest section.

Copy and paste the following code into your project.

types/general.d.ts
type allowedPrimitiveT = string | number | boolean

type optionT = {
  label: React.ReactNode
  value: allowedPrimitiveT
  className?: string
  disabled?: boolean
}

type groupT = {
  group: string
  options: (allowedPrimitiveT | optionT)[]
  className?: string
}

type optionsT = (allowedPrimitiveT | optionT | groupT)[]

type indicatorAtT = 'right' | 'left'
types/menu.d.ts
type menuOptionT =
  | allowedPrimitiveT
  | optionT
  | (optionT & {
      variant?: 'default' | 'destructive'
      shortcut?: string
      disabled?: boolean
    })

type menuGroupT = {
  group: string
  options: menuOptionT[]
  className?: string
  groupLabelCls?: string
}

type subMenuT = {
  submenu: string
  options: (menuOptionT | menuGroupT)[]
  triggerCls?: string
  contentCls?: string
}

type menuOptionsT = (menuOptionT | menuGroupT | subMenuT)[]

type menuInputOptionT =
  | allowedPrimitiveT
  | optionT
  | (optionT & {
      disabled?: boolean
    })

type menuInputGroupT = {
  group: string
  options: menuInputOptionT[]
  className?: string
  groupLabelCls?: string
}

type inputSubMenuT = {
  submenu: string
  options: (menuInputOptionT | menuInputGroupT)[]
  triggerCls?: string
  contentCls?: string
}

type menuInputOptionsT = (menuInputOptionT | menuInputGroupT | inputSubMenuT)[]
lib/utils.ts
import { isValidElement, type ReactNode } from 'react'
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

export function isAllowedPrimitive(value: unknown): value is allowedPrimitiveT {
  return ['string', 'number', 'boolean'].includes(typeof value)
}

export function parseAllowedPrimitive(value: allowedPrimitiveT): allowedPrimitiveT {
  if (typeof value !== 'string') return value

  const trimmed = value.trim()

  if (trimmed === 'true') return true
  if (trimmed === 'false') return false
  if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed)

  return trimmed
}

export function optionTypeChecker<T>(key: keyof T) {
  return (option: any): option is T => !!option && typeof option === 'object' && key in option
}

export const isSeparator = (item: any) => item === '---'
export const isOption = optionTypeChecker<optionT>('value')
export const isGroup = optionTypeChecker<groupT>('group')

export const getValue = (item: allowedPrimitiveT | optionT) =>
  typeof item === 'object' ? item.value : item
export const getLabel = (item: allowedPrimitiveT | optionT) =>
  typeof item === 'object' ? item.label : `${item}`

export function getKey(item: allowedPrimitiveT | optionT, i: number): string {
  const val = getValue(item)
  if (typeof val === 'boolean') return `key-${val}`
  if (val === '---') return `---${i}`
  return `${val}`
}

export function extractText(node: ReactNode): string {
  if (node == null || typeof node === 'boolean') return ''
  if (typeof node === 'string' || typeof node === 'number') return String(node)
  if (isValidElement(node)) return extractText((node.props as { children?: ReactNode }).children)
  if (Array.isArray(node)) return node.map(extractText).join('')
  return ''
}
lib/menu.ts
import { optionTypeChecker } from './utils'

export const isSubMenu = optionTypeChecker<subMenuT>('submenu')
export const isGroupMenu = optionTypeChecker<menuGroupT>('group')
export const isInputSubMenu = optionTypeChecker<inputSubMenuT>('submenu')
export const isInputGroupMenu = optionTypeChecker<menuInputGroupT>('group')
ui/menubar.tsx
'use client'

import * as React from 'react'
import { Menubar as MenubarPrimitive } from '@base-ui/react/menubar'
import { Menu as MenuPrimitive } from '@base-ui/react/menu'
import { CheckIcon } from 'lucide-react'

import { cn } from '@/lib/utils'

import {
  Menu,
  MenuContent,
  MenuGroup,
  MenuItem,
  MenuLabel,
  MenuPortal,
  MenuRadioGroup,
  MenuSeparator,
  MenuShortcut,
  MenuSub,
  MenuSubContent,
  MenuSubTrigger,
  MenuTrigger,
} from '@/components/ui/menu'

function Menubar({ className, ...props }: MenubarPrimitive.Props) {
  return (
    <MenubarPrimitive
      data-slot="menubar"
      className={cn('flex h-8 items-center gap-0.5 rounded-lg border p-0.75', className)}
      {...props}
    />
  )
}

function MenubarMenu(props: React.ComponentProps<typeof Menu>) {
  return <Menu data-slot="menubar-menu" {...props} />
}

function MenubarGroup(props: React.ComponentProps<typeof MenuGroup>) {
  return <MenuGroup data-slot="menubar-group" {...props} />
}

function MenubarPortal(props: React.ComponentProps<typeof MenuPortal>) {
  return <MenuPortal data-slot="menubar-portal" {...props} />
}

function MenubarTrigger({ className, ...props }: React.ComponentProps<typeof MenuTrigger>) {
  return (
    <MenuTrigger
      data-slot="menubar-trigger"
      className={cn(
        'flex items-center rounded-sm px-1.5 py-0.5 text-sm font-medium outline-hidden select-none hover:bg-muted aria-expanded:bg-muted',
        className,
      )}
      {...props}
    />
  )
}

function MenubarContent({
  className,
  align = 'start',
  alignOffset = -4,
  sideOffset = 8,
  ...props
}: React.ComponentProps<typeof MenuContent>) {
  return (
    <MenuContent
      data-slot="menubar-content"
      align={align}
      alignOffset={alignOffset}
      sideOffset={sideOffset}
      className={cn(
        'min-w-36 rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95',
        className,
      )}
      {...props}
    />
  )
}

function MenubarItem({
  className,
  inset,
  variant = 'default',
  ...props
}: React.ComponentProps<typeof MenuItem>) {
  return (
    <MenuItem
      data-slot="menubar-item"
      data-inset={inset}
      data-variant={variant}
      className={cn(
        "group/menubar-item gap-1.5 rounded-md px-1.5 py-1 text-sm focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive!",
        className,
      )}
      {...props}
    />
  )
}

function MenubarCheckboxItem({
  className,
  children,
  checked,
  inset,
  indicatorAt = 'right',
  ...props
}: MenuPrimitive.CheckboxItem.Props & {
  inset?: boolean
  indicatorAt?: indicatorAtT
}) {
  return (
    <MenuPrimitive.CheckboxItem
      data-slot="menubar-checkbox-item"
      data-inset={inset}
      className={cn(
        'relative flex cursor-default items-center gap-1.5 rounded-md py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
        className,
        indicatorAt === 'right' ? 'pr-8 pl-2' : 'pr-2 pl-8',
      )}
      checked={checked}
      {...props}
    >
      <span
        className={cn(
          "pointer-events-none absolute flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4",
          indicatorAt === 'right' ? 'right-2' : 'left-2',
        )}
      >
        <MenuPrimitive.CheckboxItemIndicator>
          <CheckIcon />
        </MenuPrimitive.CheckboxItemIndicator>
      </span>
      {children}
    </MenuPrimitive.CheckboxItem>
  )
}

function MenubarRadioGroup(props: React.ComponentProps<typeof MenuRadioGroup>) {
  return <MenuRadioGroup data-slot="menubar-radio-group" {...props} />
}

function MenubarRadioItem({
  className,
  children,
  inset,
  indicatorAt = 'right',
  ...props
}: MenuPrimitive.RadioItem.Props & {
  inset?: boolean
  indicatorAt?: indicatorAtT
}) {
  return (
    <MenuPrimitive.RadioItem
      data-slot="menubar-radio-item"
      data-inset={inset}
      className={cn(
        "relative flex cursor-default items-center gap-1.5 rounded-md py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className,
        indicatorAt === 'right' ? 'pr-8 pl-2' : 'pr-2 pl-8',
      )}
      {...props}
    >
      <span
        className={cn(
          "pointer-events-none absolute flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4",
          indicatorAt === 'right' ? 'right-2' : 'left-2',
        )}
      >
        <MenuPrimitive.RadioItemIndicator>
          <CheckIcon />
        </MenuPrimitive.RadioItemIndicator>
      </span>
      {children}
    </MenuPrimitive.RadioItem>
  )
}

function MenubarLabel({
  className,
  inset,
  ...props
}: React.ComponentProps<typeof MenuLabel> & {
  inset?: boolean
}) {
  return (
    <MenuLabel
      data-slot="menubar-label"
      data-inset={inset}
      className={cn('px-1.5 py-1 text-sm font-medium data-inset:pl-7', className)}
      {...props}
    />
  )
}

function MenubarSeparator({
  className,
  ...props
}: React.ComponentProps<typeof MenuSeparator>) {
  return (
    <MenuSeparator
      data-slot="menubar-separator"
      className={cn('-mx-1 my-1 h-px bg-border', className)}
      {...props}
    />
  )
}

function MenubarShortcut({
  className,
  ...props
}: React.ComponentProps<typeof MenuShortcut>) {
  return (
    <MenuShortcut
      data-slot="menubar-shortcut"
      className={cn(
        'ml-auto text-xs tracking-widest text-muted-foreground group-focus/menubar-item:text-accent-foreground',
        className,
      )}
      {...props}
    />
  )
}

function MenubarSub(props: React.ComponentProps<typeof MenuSub>) {
  return <MenuSub data-slot="menubar-sub" {...props} />
}

function MenubarSubTrigger({
  className,
  inset,
  ...props
}: React.ComponentProps<typeof MenuSubTrigger> & {
  inset?: boolean
}) {
  return (
    <MenuSubTrigger
      data-slot="menubar-sub-trigger"
      data-inset={inset}
      className={cn(
        "gap-1.5 rounded-md px-1.5 py-1 text-sm focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-open:bg-accent data-open:text-accent-foreground [&_svg:not([class*='size-'])]:size-4",
        className,
      )}
      {...props}
    />
  )
}

function MenubarSubContent({
  className,
  ...props
}: React.ComponentProps<typeof MenuSubContent>) {
  return (
    <MenuSubContent
      data-slot="menubar-sub-content"
      className={cn(
        'min-w-32 rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
        className,
      )}
      {...props}
    />
  )
}

export {
  Menubar,
  MenubarPortal,
  MenubarMenu,
  MenubarTrigger,
  MenubarContent,
  MenubarGroup,
  MenubarSeparator,
  MenubarLabel,
  MenubarItem,
  MenubarShortcut,
  MenubarCheckboxItem,
  MenubarRadioGroup,
  MenubarRadioItem,
  MenubarSub,
  MenubarSubTrigger,
  MenubarSubContent,
}
ui/menubar-wrapper.tsx
'use client'

import { useState } from 'react'
import {
  cn,
  getKey,
  getLabel,
  getValue,
  isSeparator,
  parseAllowedPrimitive,
} from '@/lib/utils'
import {
  isSubMenu,
  isGroupMenu,
  isInputSubMenu,
  isInputGroupMenu
} from '@/lib/menu'

import {
  Menubar,
  MenubarTrigger,
  MenubarContent,
  MenubarGroup,
  MenubarLabel,
  MenubarItem,
  MenubarCheckboxItem,
  MenubarRadioGroup,
  MenubarRadioItem,
  MenubarSeparator,
  MenubarShortcut,
  MenubarSub,
  MenubarSubTrigger,
  MenubarSubContent,
  MenubarMenu,
} from '@/components/ui/menubar'

type commomClsT = {
  itemCls?: string
  groupCls?: string
  groupLabelCls?: string
}

type commonCheckboxProps = {
  checked?: allowedPrimitiveT[]
  indicatorAt?: indicatorAtT
  onCheckedChange?: (value: allowedPrimitiveT, checked: boolean) => void
}

type commonRadioProps = {
  value?: allowedPrimitiveT
  indicatorAt?: indicatorAtT
  onValueChange?: (value: allowedPrimitiveT) => void
}

type commonInner = commomClsT & {
  trigger: React.ReactNode
  triggerCls?: string
  triggerProps?: Omit<React.ComponentProps<typeof MenubarTrigger>, 'children' | 'className'>
  contentProps?: React.ComponentProps<typeof MenubarContent>
}

type menubarBaseT = commomClsT & {
  key: string
  trigger: React.ReactNode
  triggerCls?: string
  triggerProps?: Omit<React.ComponentProps<typeof MenubarTrigger>, 'children' | 'className'>
  contentProps?: React.ComponentProps<typeof MenubarContent>
}

type menubarOptionsT = (menubarBaseT & {
  options: menuOptionsT
  onSelect?: (value: allowedPrimitiveT) => void
})[]

type menubarInputOptionT = menubarBaseT & {
  options: menuInputOptionsT
}

type menubarCheckboxOptionsT = (menubarInputOptionT & commonCheckboxProps)[]
type menubarRadioOptionsT = (menubarInputOptionT & commonRadioProps)[]

type commonWrapT = commomClsT &
  Omit<React.ComponentProps<typeof Menubar>, 'children' | 'value'> & {
    triggerCls?: string
    triggerProps?: Omit<React.ComponentProps<typeof MenubarTrigger>, 'children' | 'className'>
    contentProps?: React.ComponentProps<typeof MenubarContent>
  }

// -------

type itemProps = {
  option: menuOptionT
  className?: string
  onSelect?: () => void
}
function Item({ option, className, onSelect }: itemProps) {
  const value = getValue(option)

  if (isSeparator(value)) return <MenubarSeparator className={cn(className)} />

  const label = getLabel(option)
  const opt: any = typeof option === 'object' ? option : {}
  const shortcut = opt?.shortcut

  return (
    <MenubarItem {...opt} onSelect={onSelect} className={cn(className, opt?.className)}>
      {label}
      {shortcut && <MenubarShortcut>{shortcut}</MenubarShortcut>}
    </MenubarItem>
  )
}

type checkboxItemProps = {
  option: menuInputOptionT
  className?: string
  checked?: boolean
  onCheckedChange?: (checked: boolean) => void
  indicatorAt?: indicatorAtT
}
function CheckboxItem({
  option,
  className,
  checked = false,
  indicatorAt,
  onCheckedChange = () => { },
}: checkboxItemProps) {
  const value = getValue(option)

  if (isSeparator(value)) return <MenubarSeparator className={cn(className)} />

  const label = getLabel(option)
  const disabled = (option as any)?.disabled

  return (
    <MenubarCheckboxItem
      checked={checked}
      disabled={disabled}
      className={cn(className)}
      indicatorAt={indicatorAt}
      onCheckedChange={onCheckedChange}
    >
      {label}
    </MenubarCheckboxItem>
  )
}

type radioItemProps = {
  option: menuInputOptionT
  className?: string
  indicatorAt?: indicatorAtT
}
function RadioItem({ option, className, indicatorAt }: radioItemProps) {
  const value = getValue(option)

  if (isSeparator(value)) return <MenubarSeparator className={cn(className)} />

  const label = getLabel(option)
  const disabled = (option as any)?.disabled

  return (
    <MenubarRadioItem
      value={`${value}`}
      disabled={disabled}
      className={cn(className)}
      indicatorAt={indicatorAt}
    >
      {label}
    </MenubarRadioItem>
  )
}

type SubMenuProps = commomClsT & {
  submenu: subMenuT
  onSelect?: (value: allowedPrimitiveT) => void
}
function SubMenu({ submenu, itemCls, groupCls, groupLabelCls, onSelect }: SubMenuProps) {
  return (
    <MenubarSub>
      <MenubarSubTrigger className={cn(submenu.triggerCls)}>{submenu.submenu}</MenubarSubTrigger>

      <MenubarSubContent className={cn(submenu.contentCls)}>
        {submenu.options.map((option, i) => {
          if (isGroupMenu(option)) {
            return (
              <MenubarGroup key={option.group} className={cn(groupCls, option.className)}>
                <MenubarLabel
                  className={cn(
                    'pb-0.5 text-xs text-muted-foreground font-normal',
                    groupLabelCls,
                    option.groupLabelCls,
                  )}
                >
                  {option.group}
                </MenubarLabel>

                {option.options.map((grOpt, j) => (
                  <Item
                    key={getKey(grOpt, j)}
                    option={grOpt}
                    className={itemCls}
                    onSelect={() => onSelect?.(getValue(grOpt))}
                  />
                ))}
              </MenubarGroup>
            )
          }

          if (isSubMenu(option)) {
            return (
              <SubMenu
                key={getKey(option, i)}
                submenu={option}
                itemCls={itemCls}
                groupCls={groupCls}
                onSelect={onSelect}
              />
            )
          }

          return (
            <Item
              key={getKey(option, i)}
              option={option}
              className={itemCls}
              onSelect={() => onSelect?.(getValue(option))}
            />
          )
        })}
      </MenubarSubContent>
    </MenubarSub>
  )
}

type CheckboxSubMenuProps = commomClsT &
  commonCheckboxProps & {
    submenu: inputSubMenuT
  }
function CheckboxSubMenu({
  submenu,
  itemCls,
  groupCls,
  groupLabelCls,
  checked = [],
  indicatorAt,
  onCheckedChange = () => { },
}: CheckboxSubMenuProps) {
  return (
    <MenubarSub>
      <MenubarSubTrigger className={cn(submenu.triggerCls)}>{submenu.submenu}</MenubarSubTrigger>

      <MenubarSubContent className={cn(submenu.contentCls)}>
        {submenu.options.map((option, i) => {
          if (isInputGroupMenu(option)) {
            return (
              <MenubarGroup key={option.group} className={cn(groupCls, option.className)}>
                <MenubarLabel
                  className={cn(
                    'pb-0.5 text-xs text-muted-foreground font-normal',
                    groupLabelCls,
                    option.groupLabelCls,
                  )}
                >
                  {option.group}
                </MenubarLabel>

                {option.options.map((grOpt, j) => {
                  const v = getValue(grOpt)
                  return (
                    <CheckboxItem
                      key={getKey(grOpt, j)}
                      option={grOpt}
                      checked={checked.includes(v)}
                      className={itemCls}
                      indicatorAt={indicatorAt}
                      onCheckedChange={checked => onCheckedChange?.(v, checked)}
                    />
                  )
                })}
              </MenubarGroup>
            )
          }

          if (isInputSubMenu(option)) {
            return (
              <CheckboxSubMenu
                key={option.submenu}
                submenu={option}
                checked={checked}
                itemCls={itemCls}
                groupCls={groupCls}
                indicatorAt={indicatorAt}
                groupLabelCls={groupLabelCls}
                onCheckedChange={onCheckedChange}
              />
            )
          }

          const v = getValue(option)
          return (
            <CheckboxItem
              key={getKey(option, i)}
              option={option}
              checked={checked.includes(v)}
              className={itemCls}
              indicatorAt={indicatorAt}
              onCheckedChange={checked => onCheckedChange?.(v, checked)}
            />
          )
        })}
      </MenubarSubContent>
    </MenubarSub>
  )
}

type RadioSubMenuProps = commomClsT &
  commonRadioProps & {
    submenu: inputSubMenuT
  }
function RadioSubMenu({
  submenu,
  itemCls,
  groupCls,
  groupLabelCls,
  value = '',
  indicatorAt,
  onValueChange = () => { },
}: RadioSubMenuProps) {
  return (
    <MenubarSub>
      <MenubarSubTrigger className={cn(submenu.triggerCls)}>{submenu.submenu}</MenubarSubTrigger>

      <MenubarSubContent className={cn(submenu.contentCls)}>
        <MenubarRadioGroup value={`${value}`} onValueChange={onValueChange}>
          {submenu.options.map((option, i) => {
            if (isInputGroupMenu(option)) {
              return (
                <MenubarGroup key={option.group} className={cn(groupCls, option.className)}>
                  <MenubarLabel
                    className={cn(
                      'pb-0.5 text-xs text-muted-foreground font-normal',
                      groupLabelCls,
                      option.groupLabelCls,
                    )}
                  >
                    {option.group}
                  </MenubarLabel>

                  {option.options.map((grOpt, j) => (
                    <RadioItem
                      key={getKey(grOpt, j)}
                      option={grOpt}
                      className={itemCls}
                      indicatorAt={indicatorAt}
                    />
                  ))}
                </MenubarGroup>
              )
            }

            if (isInputSubMenu(option)) {
              return (
                <RadioSubMenu
                  key={option.submenu}
                  value={value}
                  submenu={option}
                  itemCls={itemCls}
                  groupCls={groupCls}
                  indicatorAt={indicatorAt}
                  groupLabelCls={groupLabelCls}
                  onValueChange={onValueChange}
                />
              )
            }

            return (
              <RadioItem
                key={getKey(option, i)}
                option={option}
                className={itemCls}
                indicatorAt={indicatorAt}
              />
            )
          })}
        </MenubarRadioGroup>
      </MenubarSubContent>
    </MenubarSub>
  )
}

type wrapperInner = commonInner & {
  options: menuOptionsT
  onSelect?: (value: allowedPrimitiveT) => void
}
function MenubarWrapperInner({
  trigger,
  options,
  triggerCls,
  triggerProps,
  itemCls,
  groupCls,
  groupLabelCls,
  contentProps,
  onSelect,
}: wrapperInner) {
  return (
    <MenubarMenu>
      <MenubarTrigger className={triggerCls} {...triggerProps}>{trigger}</MenubarTrigger>

      <MenubarContent {...contentProps}>
        {options.map((option, i) => {
          if (isGroupMenu(option)) {
            return (
              <MenubarGroup key={option.group} className={cn(groupCls, option.className)}>
                <MenubarLabel
                  className={cn(
                    'pb-0.5 text-xs text-muted-foreground font-normal',
                    groupLabelCls,
                    option.groupLabelCls,
                  )}
                >
                  {option.group}
                </MenubarLabel>

                {option.options.map((grOpt, j) => (
                  <Item
                    key={getKey(grOpt, j)}
                    option={grOpt}
                    className={itemCls}
                    onSelect={() => onSelect?.(getValue(grOpt))}
                  />
                ))}
              </MenubarGroup>
            )
          }

          if (isSubMenu(option)) {
            return (
              <SubMenu
                key={option.submenu}
                submenu={option}
                itemCls={itemCls}
                groupCls={groupCls}
                groupLabelCls={groupLabelCls}
                onSelect={onSelect}
              />
            )
          }

          return (
            <Item
              key={getKey(option, i)}
              option={option}
              className={itemCls}
              onSelect={() => onSelect?.(getValue(option))}
            />
          )
        })}
      </MenubarContent>
    </MenubarMenu>
  )
}

type checkboxWrapperInner = commonInner &
  commonCheckboxProps & {
    options: menuInputOptionsT
  }
function MenubarCheckboxWrapperInner({
  trigger,
  options,

  triggerCls,
  triggerProps,
  contentProps,
  itemCls,
  groupCls,
  groupLabelCls,

  checked: o_checked,
  onCheckedChange: o_onCheckedChange,

  indicatorAt,
}: checkboxWrapperInner) {
  const [i_checked, setIChecked] = useState<allowedPrimitiveT[]>([])

  function i_Checked(v: allowedPrimitiveT, c: boolean) {
    setIChecked(prev => (!c ? prev.filter(p => p !== v) : [...prev, v]))
  }

  const checked = o_checked ?? i_checked
  const onCheckedChange = o_onCheckedChange ?? i_Checked

  return (
    <MenubarMenu>
      <MenubarTrigger className={triggerCls} {...triggerProps}>{trigger}</MenubarTrigger>

      <MenubarContent {...contentProps}>
        {options.map((option, i) => {
          if (isInputGroupMenu(option)) {
            return (
              <MenubarGroup key={option.group} className={cn(groupCls, option.className)}>
                <MenubarLabel
                  className={cn(
                    'pb-0.5 text-xs text-muted-foreground font-normal',
                    groupLabelCls,
                    option.groupLabelCls,
                  )}
                >
                  {option.group}
                </MenubarLabel>

                {option.options.map((grOpt, j) => {
                  const v = getValue(grOpt)
                  return (
                    <CheckboxItem
                      key={getKey(grOpt, j)}
                      option={grOpt}
                      checked={checked.includes(v)}
                      className={itemCls}
                      indicatorAt={indicatorAt}
                      onCheckedChange={checked => onCheckedChange?.(v, checked)}
                    />
                  )
                })}
              </MenubarGroup>
            )
          }

          if (isInputSubMenu(option)) {
            return (
              <CheckboxSubMenu
                key={option.submenu}
                submenu={option}
                checked={checked}
                itemCls={itemCls}
                groupCls={groupCls}
                indicatorAt={indicatorAt}
                groupLabelCls={groupLabelCls}
                onCheckedChange={onCheckedChange}
              />
            )
          }

          const v = getValue(option)
          return (
            <CheckboxItem
              key={getKey(option, i)}
              option={option}
              checked={checked.includes(v)}
              className={itemCls}
              indicatorAt={indicatorAt}
              onCheckedChange={checked => onCheckedChange?.(v, checked)}
            />
          )
        })}
      </MenubarContent>
    </MenubarMenu>
  )
}

type radioWrapperInner = commonInner &
  commonRadioProps & {
    options: menuInputOptionsT
  }
function MenubarRadioWrapperInner({
  trigger,
  options,

  triggerCls,
  triggerProps,
  itemCls,
  groupCls,
  groupLabelCls,
  contentProps,

  value: o_value,
  onValueChange: o_onValueChange,

  indicatorAt,
}: radioWrapperInner) {
  const [i_value, setIValue] = useState<allowedPrimitiveT>('')

  const value = o_value ?? i_value
  const onValueChange = o_onValueChange ?? setIValue

  return (
    <MenubarMenu>
      <MenubarTrigger className={cn(triggerCls)} {...triggerProps}>{trigger}</MenubarTrigger>

      <MenubarContent {...contentProps}>
        <MenubarRadioGroup
          value={`${value}`}
          onValueChange={v => onValueChange(parseAllowedPrimitive(v))}
        >
          {options.map((option, i) => {
            if (isInputGroupMenu(option)) {
              return (
                <MenubarGroup key={option.group} className={cn(groupCls, option.className)}>
                  <MenubarLabel
                    className={cn(
                      'pb-0.5 text-xs text-muted-foreground font-normal',
                      groupLabelCls,
                      option.groupLabelCls,
                    )}
                  >
                    {option.group}
                  </MenubarLabel>

                  {option.options.map((grOpt, j) => (
                    <RadioItem
                      key={getKey(grOpt, j)}
                      option={grOpt}
                      className={itemCls}
                      indicatorAt={indicatorAt}
                    />
                  ))}
                </MenubarGroup>
              )
            }

            if (isInputSubMenu(option)) {
              return (
                <RadioSubMenu
                  key={option.submenu}
                  value={value}
                  submenu={option}
                  itemCls={itemCls}
                  groupCls={groupCls}
                  indicatorAt={indicatorAt}
                  groupLabelCls={groupLabelCls}
                  onValueChange={v => onValueChange(parseAllowedPrimitive(v))}
                />
              )
            }

            return (
              <RadioItem
                key={getKey(option, i)}
                option={option}
                className={itemCls}
                indicatorAt={indicatorAt}
              />
            )
          })}
        </MenubarRadioGroup>
      </MenubarContent>
    </MenubarMenu>
  )
}

type wrap = commonWrapT & {
  options: menubarOptionsT
  onSelect?: (value: allowedPrimitiveT) => void
}
function MenubarWrapper({
  options,
  triggerCls,
  triggerProps,
  itemCls,
  groupCls,
  groupLabelCls,
  contentProps,
  onSelect,
  ...props
}: wrap) {
  return (
    <Menubar {...props}>
      {options.map(op => (
        <MenubarWrapperInner
          key={op.key}
          trigger={op.trigger}
          options={op.options}
          triggerCls={cn(triggerCls, op.triggerCls)}
          triggerProps={{ ...triggerProps, ...op.triggerProps }}
          itemCls={cn(itemCls, op.itemCls)}
          groupCls={cn(groupCls, op.groupCls)}
          groupLabelCls={cn(groupLabelCls, op.groupLabelCls)}
          contentProps={{ ...contentProps, ...op?.contentProps }}
          onSelect={op?.onSelect || onSelect}
        />
      ))}
    </Menubar>
  )
}

type wrapCheckboxT = commonWrapT &
  commonCheckboxProps & {
    options: menubarCheckboxOptionsT
  }
function MenubarCheckboxWrapper({
  options,

  triggerCls,
  triggerProps,
  contentProps,
  itemCls,
  groupCls,
  groupLabelCls,

  checked,
  onCheckedChange,

  indicatorAt,
  ...props
}: wrapCheckboxT) {
  return (
    <Menubar {...props}>
      {options.map(op => (
        <MenubarCheckboxWrapperInner
          key={op.key}
          trigger={op.trigger}
          options={op.options}
          triggerCls={cn(triggerCls, op.triggerCls)}
          triggerProps={{ ...triggerProps, ...op.triggerProps }}
          itemCls={cn(itemCls, op.itemCls)}
          groupCls={cn(groupCls, op.groupCls)}
          groupLabelCls={cn(groupLabelCls, op.groupLabelCls)}
          contentProps={{ ...contentProps, ...op?.contentProps }}
          onCheckedChange={op.onCheckedChange ?? onCheckedChange}
          indicatorAt={op.indicatorAt ?? indicatorAt}
          checked={op.checked ?? checked}
        />
      ))}
    </Menubar>
  )
}

type wrapRadioT = commonWrapT &
  commonRadioProps & {
    options: menubarRadioOptionsT
  }
function MenubarRadioWrapper({
  options,

  triggerCls,
  triggerProps,
  contentProps,
  itemCls,
  groupCls,
  groupLabelCls,

  value,
  onValueChange,

  indicatorAt,
  ...props
}: wrapRadioT) {
  return (
    <Menubar {...props}>
      {options.map(op => (
        <MenubarRadioWrapperInner
          key={op.key}
          trigger={op.trigger}
          options={op.options}
          triggerCls={cn(triggerCls, op.triggerCls)}
          triggerProps={{ ...triggerProps, ...op.triggerProps }}
          itemCls={cn(itemCls, op.itemCls)}
          groupCls={cn(groupCls, op.groupCls)}
          groupLabelCls={cn(groupLabelCls, op.groupLabelCls)}
          contentProps={{ ...contentProps, ...op?.contentProps }}
          onValueChange={op.onValueChange ?? onValueChange}
          indicatorAt={op.indicatorAt ?? indicatorAt}
          value={op.value ?? value}
        />
      ))}
    </Menubar>
  )
}

export {
  MenubarWrapper,
  MenubarCheckboxWrapper,
  MenubarRadioWrapper,
  type menubarOptionsT,
  type menubarCheckboxOptionsT,
  type menubarRadioOptionsT,
}

Usage

Basic

import { type menubarOptionsT, MenubarWrapper } from "@/components/ui/menubar-wrapper"

const opts: menubarOptionsT = [
  { key: "file", trigger: "File", options: ["New", "Open", "Save"] },
  { key: "edit", trigger: "Edit", options: ["Cut", "Copy", "Paste"] },
]

<MenubarWrapper options={opts} />

Checkbox

import { type menubarCheckboxOptionsT, MenubarCheckboxWrapper } from "@/components/ui/menubar-wrapper"

const [checked, setChecked] = useState<allowedPrimitiveT[]>([])

const opts: menubarCheckboxOptionsT = [
  { key: "view", trigger: "View", options: ["Sidebar", "Toolbar", "Status Bar"] },
]

<MenubarCheckboxWrapper
  options={opts}
  checked={checked}
  onCheckedChange={(v, c) =>
    setChecked(prev => (c ? [...prev, v] : prev.filter(x => x !== v)))
  }
/>

Radio

import { type menubarRadioOptionsT, MenubarRadioWrapper } from "@/components/ui/menubar-wrapper"

const [val, setVal] = useState<allowedPrimitiveT>("Light")

const opts: menubarRadioOptionsT = [
  { key: "theme", trigger: "Theme", options: ["Light", "Dark", "System"] },
]

<MenubarRadioWrapper options={opts} value={val} onValueChange={setVal} />

Indicator position

<MenubarCheckboxWrapper options={opts} indicatorAt="right" />
<MenubarRadioWrapper options={opts} indicatorAt="right" />

Per-menu overrides

Each menu item in the options array can override global props:

const opts: menubarOptionsT = [
  {
    key: "file",
    trigger: "File",
    options: fileOptions,
    triggerCls: "font-semibold",
    onSelect: v => handleFileAction(v),
    contentProps: { align: "start" },
  },
  {
    key: "edit",
    trigger: "Edit",
    options: editOptions,
  },
]

Complex options (groups, submenus, separators)

const opts: menubarOptionsT = [
  {
    key: "file",
    trigger: "File",
    options: [
      { label: "New", value: "new", shortcut: "⌘N" },
      "Open",
      "---",
      { group: "Recent", options: ["file1.txt", "file2.txt"] },
      { submenu: "Export", options: ["PDF", "HTML"] },
    ],
  },
]

Reference

Prop

Type

Prop

Type

Prop

Type