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/menubarIf you haven't set up the prerequisites yet, check out Prerequest section.
Copy and paste the following code into your project.
type allowedPrimitiveT = string | number | boolean
type itemT = {
label: React.ReactNode
value: allowedPrimitiveT
className?: string
disabled?: boolean
}
type groupT = {
group: string
items: (allowedPrimitiveT | itemT)[]
className?: string
}
type itemsT = (allowedPrimitiveT | itemT | groupT)[]
type indicatorAtT = 'right' | 'left' | ''
type menuItemT =
| allowedPrimitiveT
| itemT
| (itemT & {
variant?: 'default' | 'destructive'
shortcut?: string
disabled?: boolean
})
type menuGroupT = {
group: string
items: menuItemT[]
className?: string
groupLabelCls?: string
}
type subMenuT = {
submenu: string
items: (menuItemT | menuGroupT)[]
triggerCls?: string
contentCls?: string
}
type menuItemsT = (menuItemT | menuGroupT | subMenuT)[]
type menuInputItemT =
| allowedPrimitiveT
| itemT
| (itemT & {
disabled?: boolean
})
type menuInputGroupT = {
group: string
items: menuInputItemT[]
className?: string
groupLabelCls?: string
}
type inputSubMenuT = {
submenu: string
items: (menuInputItemT | menuInputGroupT)[]
triggerCls?: string
contentCls?: string
}
type menuInputItemsT = (menuInputItemT | menuInputGroupT | inputSubMenuT)[]
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 itemTypeChecker<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 = itemTypeChecker<itemT>('value')
export const isGroup = itemTypeChecker<groupT>('group')
export const getValue = (item: allowedPrimitiveT | itemT) =>
typeof item === 'object' ? item.value : item
export const getLabel = (item: allowedPrimitiveT | itemT) =>
typeof item === 'object' ? item.label : `${item}`
export function getKey(item: allowedPrimitiveT | itemT, 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 ''
}
import { itemTypeChecker } from './utils'
export const isSubMenu = itemTypeChecker<subMenuT>('submenu')
export const isGroupMenu = itemTypeChecker<menuGroupT>('group')
export const isInputSubMenu = itemTypeChecker<inputSubMenuT>('submenu')
export const isInputGroupMenu = itemTypeChecker<menuInputGroupT>('group')
'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 === '' ? 'px-2' : indicatorAt === 'right' ? 'pr-8 pl-2' : 'pr-2 pl-8',
)}
checked={checked}
{...props}
>
{indicatorAt !== '' && (
<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 === '' ? 'px-2' : indicatorAt === 'right' ? 'pr-8 pl-2' : 'pr-2 pl-8',
)}
{...props}
>
{indicatorAt !== '' && (
<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,
}
'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 menubaritemsT = (menubarBaseT & {
items: menuItemsT
onSelect?: (value: allowedPrimitiveT) => void
})[]
type menubarInputitemT = menubarBaseT & {
items: menuInputItemsT
}
type menubarCheckboxitemsT = (menubarInputitemT & commonCheckboxProps)[]
type menubarRadioitemsT = (menubarInputitemT & 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 = {
item: menuItemT
className?: string
onSelect?: () => void
}
function Item({ item, className, onSelect }: itemProps) {
const value = getValue(item)
if (isSeparator(value)) return <MenubarSeparator className={cn(className)} />
const label = getLabel(item)
const opt: any = typeof item === 'object' ? item : {}
const shortcut = opt?.shortcut
return (
<MenubarItem {...opt} onSelect={onSelect} className={cn(className, opt?.className)}>
{label}
{shortcut && <MenubarShortcut>{shortcut}</MenubarShortcut>}
</MenubarItem>
)
}
type checkboxItemProps = {
item: menuInputItemT
checked?: boolean
className?: string
indicatorAt?: indicatorAtT
onCheckedChange?: (checked: boolean) => void
}
function CheckboxItem({
item,
className,
checked = false,
indicatorAt,
onCheckedChange = () => {},
}: checkboxItemProps) {
const value = getValue(item)
if (isSeparator(value)) return <MenubarSeparator className={cn(className)} />
const label = getLabel(item)
const disabled = (item as any)?.disabled
return (
<MenubarCheckboxItem
checked={checked}
disabled={disabled}
className={cn(className)}
indicatorAt={indicatorAt}
onCheckedChange={onCheckedChange}
>
{label}
</MenubarCheckboxItem>
)
}
type radioItemProps = {
item: menuInputItemT
className?: string
indicatorAt?: indicatorAtT
}
function RadioItem({ item, className, indicatorAt }: radioItemProps) {
const value = getValue(item)
if (isSeparator(value)) return <MenubarSeparator className={cn(className)} />
const label = getLabel(item)
const disabled = (item 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.items.map((item, i) => {
if (isGroupMenu(item)) {
return (
<MenubarGroup key={item.group} className={cn(groupCls, item.className)}>
<MenubarLabel
className={cn(
'pb-0.5 text-xs text-muted-foreground font-normal',
groupLabelCls,
item.groupLabelCls,
)}
>
{item.group}
</MenubarLabel>
{item.items.map((grOpt, j) => (
<Item
key={getKey(grOpt, j)}
item={grOpt}
className={itemCls}
onSelect={() => onSelect?.(getValue(grOpt))}
/>
))}
</MenubarGroup>
)
}
if (isSubMenu(item)) {
return (
<SubMenu
key={item.submenu}
submenu={item}
itemCls={itemCls}
groupCls={groupCls}
onSelect={onSelect}
/>
)
}
return (
<Item
key={getKey(item, i)}
item={item}
className={itemCls}
onSelect={() => onSelect?.(getValue(item))}
/>
)
})}
</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.items.map((item, i) => {
if (isInputGroupMenu(item)) {
return (
<MenubarGroup key={item.group} className={cn(groupCls, item.className)}>
<MenubarLabel
className={cn(
'pb-0.5 text-xs text-muted-foreground font-normal',
groupLabelCls,
item.groupLabelCls,
)}
>
{item.group}
</MenubarLabel>
{item.items.map((grOpt, j) => {
const v = getValue(grOpt)
return (
<CheckboxItem
key={getKey(grOpt, j)}
item={grOpt}
checked={checked.includes(v)}
className={itemCls}
indicatorAt={indicatorAt}
onCheckedChange={checked => onCheckedChange?.(v, checked)}
/>
)
})}
</MenubarGroup>
)
}
if (isInputSubMenu(item)) {
return (
<CheckboxSubMenu
key={item.submenu}
submenu={item}
checked={checked}
itemCls={itemCls}
groupCls={groupCls}
indicatorAt={indicatorAt}
groupLabelCls={groupLabelCls}
onCheckedChange={onCheckedChange}
/>
)
}
const v = getValue(item)
return (
<CheckboxItem
key={getKey(item, i)}
item={item}
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.items.map((item, i) => {
if (isInputGroupMenu(item)) {
return (
<MenubarGroup key={item.group} className={cn(groupCls, item.className)}>
<MenubarLabel
className={cn(
'pb-0.5 text-xs text-muted-foreground font-normal',
groupLabelCls,
item.groupLabelCls,
)}
>
{item.group}
</MenubarLabel>
{item.items.map((grOpt, j) => (
<RadioItem
key={getKey(grOpt, j)}
item={grOpt}
className={itemCls}
indicatorAt={indicatorAt}
/>
))}
</MenubarGroup>
)
}
if (isInputSubMenu(item)) {
return (
<RadioSubMenu
key={item.submenu}
value={value}
submenu={item}
itemCls={itemCls}
groupCls={groupCls}
indicatorAt={indicatorAt}
groupLabelCls={groupLabelCls}
onValueChange={onValueChange}
/>
)
}
return (
<RadioItem
key={getKey(item, i)}
item={item}
className={itemCls}
indicatorAt={indicatorAt}
/>
)
})}
</MenubarRadioGroup>
</MenubarSubContent>
</MenubarSub>
)
}
type wrapperInner = commonInner & {
items: menuItemsT
onSelect?: (value: allowedPrimitiveT) => void
}
function MenubarWrapperInner({
trigger,
items,
triggerCls,
triggerProps,
itemCls,
groupCls,
groupLabelCls,
contentProps,
onSelect,
}: wrapperInner) {
return (
<MenubarMenu>
<MenubarTrigger className={triggerCls} {...triggerProps}>
{trigger}
</MenubarTrigger>
<MenubarContent {...contentProps}>
{items.map((item, i) => {
if (isGroupMenu(item)) {
return (
<MenubarGroup key={item.group} className={cn(groupCls, item.className)}>
<MenubarLabel
className={cn(
'pb-0.5 text-xs text-muted-foreground font-normal',
groupLabelCls,
item.groupLabelCls,
)}
>
{item.group}
</MenubarLabel>
{item.items.map((grOpt, j) => (
<Item
key={getKey(grOpt, j)}
item={grOpt}
className={itemCls}
onSelect={() => onSelect?.(getValue(grOpt))}
/>
))}
</MenubarGroup>
)
}
if (isSubMenu(item)) {
return (
<SubMenu
key={item.submenu}
submenu={item}
itemCls={itemCls}
groupCls={groupCls}
groupLabelCls={groupLabelCls}
onSelect={onSelect}
/>
)
}
return (
<Item
key={getKey(item, i)}
item={item}
className={itemCls}
onSelect={() => onSelect?.(getValue(item))}
/>
)
})}
</MenubarContent>
</MenubarMenu>
)
}
type checkboxWrapperInner = commonInner &
commonCheckboxProps & {
items: menuInputItemsT
}
function MenubarCheckboxWrapperInner({
trigger,
items,
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}>
{items.map((item, i) => {
if (isInputGroupMenu(item)) {
return (
<MenubarGroup key={item.group} className={cn(groupCls, item.className)}>
<MenubarLabel
className={cn(
'pb-0.5 text-xs text-muted-foreground font-normal',
groupLabelCls,
item.groupLabelCls,
)}
>
{item.group}
</MenubarLabel>
{item.items.map((grOpt, j) => {
const v = getValue(grOpt)
return (
<CheckboxItem
key={getKey(grOpt, j)}
item={grOpt}
checked={checked.includes(v)}
className={itemCls}
indicatorAt={indicatorAt}
onCheckedChange={checked => onCheckedChange?.(v, checked)}
/>
)
})}
</MenubarGroup>
)
}
if (isInputSubMenu(item)) {
return (
<CheckboxSubMenu
key={item.submenu}
submenu={item}
checked={checked}
itemCls={itemCls}
groupCls={groupCls}
indicatorAt={indicatorAt}
groupLabelCls={groupLabelCls}
onCheckedChange={onCheckedChange}
/>
)
}
const v = getValue(item)
return (
<CheckboxItem
key={getKey(item, i)}
item={item}
checked={checked.includes(v)}
className={itemCls}
indicatorAt={indicatorAt}
onCheckedChange={checked => onCheckedChange?.(v, checked)}
/>
)
})}
</MenubarContent>
</MenubarMenu>
)
}
type radioWrapperInner = commonInner &
commonRadioProps & {
items: menuInputItemsT
}
function MenubarRadioWrapperInner({
trigger,
items,
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))}
>
{items.map((item, i) => {
if (isInputGroupMenu(item)) {
return (
<MenubarGroup key={item.group} className={cn(groupCls, item.className)}>
<MenubarLabel
className={cn(
'pb-0.5 text-xs text-muted-foreground font-normal',
groupLabelCls,
item.groupLabelCls,
)}
>
{item.group}
</MenubarLabel>
{item.items.map((grOpt, j) => (
<RadioItem
key={getKey(grOpt, j)}
item={grOpt}
className={itemCls}
indicatorAt={indicatorAt}
/>
))}
</MenubarGroup>
)
}
if (isInputSubMenu(item)) {
return (
<RadioSubMenu
key={item.submenu}
value={value}
submenu={item}
itemCls={itemCls}
groupCls={groupCls}
indicatorAt={indicatorAt}
groupLabelCls={groupLabelCls}
onValueChange={v => onValueChange(parseAllowedPrimitive(v))}
/>
)
}
return (
<RadioItem
key={getKey(item, i)}
item={item}
className={itemCls}
indicatorAt={indicatorAt}
/>
)
})}
</MenubarRadioGroup>
</MenubarContent>
</MenubarMenu>
)
}
type wrap = commonWrapT & {
items: menubaritemsT
onSelect?: (value: allowedPrimitiveT) => void
}
function MenubarWrapper({
items,
triggerCls,
triggerProps,
itemCls,
groupCls,
groupLabelCls,
contentProps,
onSelect,
...props
}: wrap) {
return (
<Menubar {...props}>
{items.map(op => (
<MenubarWrapperInner
key={op.key}
trigger={op.trigger}
items={op.items}
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 & {
items: menubarCheckboxitemsT
}
function MenubarCheckboxWrapper({
items,
triggerCls,
triggerProps,
contentProps,
itemCls,
groupCls,
groupLabelCls,
checked,
onCheckedChange,
indicatorAt,
...props
}: wrapCheckboxT) {
return (
<Menubar {...props}>
{items.map(op => (
<MenubarCheckboxWrapperInner
key={op.key}
trigger={op.trigger}
items={op.items}
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 & {
items: menubarRadioitemsT
}
function MenubarRadioWrapper({
items,
triggerCls,
triggerProps,
contentProps,
itemCls,
groupCls,
groupLabelCls,
value,
onValueChange,
indicatorAt,
...props
}: wrapRadioT) {
return (
<Menubar {...props}>
{items.map(op => (
<MenubarRadioWrapperInner
key={op.key}
trigger={op.trigger}
items={op.items}
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 menubaritemsT,
type menubarCheckboxitemsT,
type menubarRadioitemsT,
}
Usage
Basic
import { type menubaritemsT, MenubarWrapper } from "@/components/ui/menubar-wrapper"
const opts: menubaritemsT = [
{ key: "file", trigger: "File", items: ["New", "Open", "Save"] },
{ key: "edit", trigger: "Edit", items: ["Cut", "Copy", "Paste"] },
]
<MenubarWrapper items={opts} />Checkbox
import { type menubarCheckboxitemsT, MenubarCheckboxWrapper } from "@/components/ui/menubar-wrapper"
const [checked, setChecked] = useState<allowedPrimitiveT[]>([])
const opts: menubarCheckboxitemsT = [
{ key: "view", trigger: "View", items: ["Sidebar", "Toolbar", "Status Bar"] },
]
<MenubarCheckboxWrapper
items={opts}
checked={checked}
onCheckedChange={(v, c) =>
setChecked(prev => (c ? [...prev, v] : prev.filter(x => x !== v)))
}
/>Radio
import { type menubarRadioitemsT, MenubarRadioWrapper } from "@/components/ui/menubar-wrapper"
const [val, setVal] = useState<allowedPrimitiveT>("Light")
const opts: menubarRadioitemsT = [
{ key: "theme", trigger: "Theme", items: ["Light", "Dark", "System"] },
]
<MenubarRadioWrapper items={opts} value={val} onValueChange={setVal} />Indicator position
<MenubarCheckboxWrapper items={opts} indicatorAt="right" />
<MenubarRadioWrapper items={opts} indicatorAt="right" />Per-menu overrides
Each menu item in the items array can override global props:
const opts: menubaritemsT = [
{
key: "file",
trigger: "File",
items: fileItems,
triggerCls: "font-semibold",
onSelect: v => handleFileAction(v),
contentProps: { align: "start" },
},
{
key: "edit",
trigger: "Edit",
items: editItems,
},
]Complex items (groups, submenus, separators)
const opts: menubaritemsT = [
{
key: "file",
trigger: "File",
items: [
{ label: "New", value: "new", shortcut: "⌘N" },
"Open",
"---",
{ group: "Recent", items: ["file1.txt", "file2.txt"] },
{ submenu: "Export", items: ["PDF", "HTML"] },
],
},
]Reference
MenubarWrapper
Prop
Type
MenubarCheckboxWrapper
Prop
Type
MenubarRadioWrapper
Prop
Type
Related Components
- Base UI Menubar
- Shadcn Menubar
- Menu — single dropdown, same items API