Menubar
A row of menus, the way a desktop app's File and Edit menus sit.
Vibra draws each menu as the kit's elev-1 floating layer, fading in over --duration-base without shadcn's zoom and sliding 4px rather than 8; items, checks and choices alike, are set in the 13px label register. The menus themselves are Vibra's dropdown menu parts. The bar is one tab stop: Left and Right walk it, Down or Enter opens a menu, and with one open the arrows move to the next menu, open. The triggers wear the kit's focus ring, where shadcn's show nothing to a keyboard, and every part names logical sides, so under DirectionProvider the bar reads from the right and its arrows turn with it.
Install
npx shadcn@latest add @vibra/menubarNeeds the @vibra registry in your components.json — set it up once.
Examples
import {
Menubar,
MenubarContent,
MenubarGroup,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarShortcut,
MenubarTrigger,
} from "@/components/ui/menubar"
export default function MenubarDemo() {
return (
<Menubar aria-label="Report editor">
<MenubarMenu>
<MenubarTrigger>Report</MenubarTrigger>
<MenubarContent>
<MenubarGroup>
<MenubarItem>
New report
<MenubarShortcut>⌘N</MenubarShortcut>
</MenubarItem>
<MenubarItem>Duplicate</MenubarItem>
</MenubarGroup>
<MenubarSeparator />
<MenubarItem>
Export as CSV
<MenubarShortcut>⌘E</MenubarShortcut>
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>View</MenubarTrigger>
<MenubarContent>
<MenubarItem>Compare to last period</MenubarItem>
<MenubarItem>Show gridlines</MenubarItem>
<MenubarSeparator />
<MenubarItem>
Full screen
<MenubarShortcut>⌘⇧F</MenubarShortcut>
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Share</MenubarTrigger>
<MenubarContent>
<MenubarItem>Copy link</MenubarItem>
<MenubarItem>Schedule an email</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>
)
}Checks and choices
A chart editor's View and Compare menus: layers as checkbox items that stay open, and one comparison as radio items that close, said in the line below.
"use client"
import * as React from "react"
import {
Menubar,
MenubarCheckboxItem,
MenubarContent,
MenubarGroup,
MenubarLabel,
MenubarMenu,
MenubarRadioGroup,
MenubarRadioItem,
MenubarTrigger,
} from "@/components/ui/menubar"
const LAYERS = ["Gridlines", "Legend", "Targets"]
const COMPARE = [
{ value: "previous", label: "Previous period" },
{ value: "last-year", label: "Same period last year" },
{ value: "none", label: "Nothing" },
]
// A chart editor's settings, as a desktop app keeps them: what shows is a
// set of checks, what to compare with is one choice. Checks stay open for the
// next toggle; the menus answer Left and Right, so the whole bar is reachable
// from one tab stop.
export default function MenubarChecks() {
const [layers, setLayers] = React.useState(["Gridlines", "Legend"])
const [compare, setCompare] = React.useState("previous")
return (
<div className="flex flex-col items-center gap-3">
<Menubar aria-label="Revenue chart">
<MenubarMenu>
<MenubarTrigger>View</MenubarTrigger>
<MenubarContent className="w-48">
<MenubarGroup>
<MenubarLabel>Show</MenubarLabel>
{LAYERS.map((layer) => (
<MenubarCheckboxItem
key={layer}
checked={layers.includes(layer)}
onCheckedChange={(on) => setLayers((current) => (on ? [...current, layer] : current.filter((name) => name !== layer)))}
>
{layer}
</MenubarCheckboxItem>
))}
</MenubarGroup>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Compare</MenubarTrigger>
<MenubarContent className="w-56">
<MenubarRadioGroup value={compare} onValueChange={(value) => setCompare(String(value))}>
<MenubarLabel>Compare September with</MenubarLabel>
{COMPARE.map((option) => (
<MenubarRadioItem key={option.value} value={option.value} closeOnClick>
{option.label}
</MenubarRadioItem>
))}
</MenubarRadioGroup>
</MenubarContent>
</MenubarMenu>
</Menubar>
<p role="status" className="text-xs text-muted-foreground">
{layers.length ? layers.join(", ") : "No layers"} ·{" "}
{compare === "none" ? "no comparison" : `compared with ${COMPARE.find((option) => option.value === compare)!.label.toLowerCase()}`}
</p>
</div>
)
}Submenus, shortcuts and a disabled item
File and Edit as a desktop app keeps them: recent reports and export formats one level down, shortcuts at the end of each line, and Redo dimmed in its place.
import {
Menubar,
MenubarContent,
MenubarGroup,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarShortcut,
MenubarSub,
MenubarSubContent,
MenubarSubTrigger,
MenubarTrigger,
} from "@/components/ui/menubar"
const RECENT = ["Q3 revenue", "Churn by plan", "Board pack, September"]
// The shape of a desktop app's File and Edit menus: shortcuts at the end of
// each line, lists that open to the side, and an item that can't be used
// yet, dimmed but still in its place so the menu doesn't reflow — Redo, with
// nothing undone to redo.
export default function MenubarSubmenus() {
return (
<Menubar aria-label="Report editor">
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent className="w-52">
<MenubarGroup>
<MenubarItem>
New report
<MenubarShortcut>⌘N</MenubarShortcut>
</MenubarItem>
<MenubarSub>
<MenubarSubTrigger>Open recent</MenubarSubTrigger>
<MenubarSubContent className="w-52">
{RECENT.map((name) => (
<MenubarItem key={name} render={<a href={`/reports/${name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`} />}>
{name}
</MenubarItem>
))}
</MenubarSubContent>
</MenubarSub>
</MenubarGroup>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>Export as</MenubarSubTrigger>
<MenubarSubContent className="w-36">
<MenubarItem>CSV</MenubarItem>
<MenubarItem>PDF</MenubarItem>
<MenubarItem>PNG image</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarItem>
Print
<MenubarShortcut>⌘P</MenubarShortcut>
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Edit</MenubarTrigger>
<MenubarContent className="w-52">
<MenubarItem>
Undo rename
<MenubarShortcut>⌘Z</MenubarShortcut>
</MenubarItem>
<MenubarItem disabled>
Redo
<MenubarShortcut>⇧⌘Z</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem>
Duplicate chart
<MenubarShortcut>⌘D</MenubarShortcut>
</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>
)
}Right to left
The bar reads from the right: ticks lead at the right, shortcuts trail at the left, a submenu opens to the left, and Left moves on to the next menu.
"use client"
import * as React from "react"
import { DirectionProvider } from "@/components/ui/direction"
import {
Menubar,
MenubarCheckboxItem,
MenubarContent,
MenubarGroup,
MenubarItem,
MenubarMenu,
MenubarShortcut,
MenubarSub,
MenubarSubContent,
MenubarSubTrigger,
MenubarTrigger,
} from "@/components/ui/menubar"
// The bar reads from the right, File first. Ticks lead at the right of their
// lines, shortcuts trail at the left, and "Export as" opens to the left.
// DirectionProvider turns the arrows round with it: Left moves on to the next
// menu, and opens a submenu.
export default function MenubarRtl() {
const [grid, setGrid] = React.useState(true)
const [legend, setLegend] = React.useState(false)
return (
<DirectionProvider direction="rtl">
<div dir="rtl" lang="ar">
<Menubar aria-label="محرر التقارير">
<MenubarMenu>
<MenubarTrigger>ملف</MenubarTrigger>
<MenubarContent dir="rtl" lang="ar" className="w-48">
<MenubarItem>
تقرير جديد
<MenubarShortcut>⌘N</MenubarShortcut>
</MenubarItem>
<MenubarSub>
<MenubarSubTrigger>تصدير بصيغة</MenubarSubTrigger>
<MenubarSubContent dir="rtl" lang="ar" className="w-32">
<MenubarItem>CSV</MenubarItem>
<MenubarItem>PDF</MenubarItem>
</MenubarSubContent>
</MenubarSub>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>عرض</MenubarTrigger>
<MenubarContent dir="rtl" lang="ar" className="w-48">
<MenubarGroup>
<MenubarCheckboxItem checked={grid} onCheckedChange={setGrid}>
خطوط الشبكة
</MenubarCheckboxItem>
<MenubarCheckboxItem checked={legend} onCheckedChange={setLegend}>
مفتاح الرسم
</MenubarCheckboxItem>
</MenubarGroup>
</MenubarContent>
</MenubarMenu>
</Menubar>
</div>
</DirectionProvider>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| MenubarItem.variant | "default" | "destructive" | "default" | destructive sets the item in the danger tone. |
| MenubarItem.inset | boolean | — | Indents the item to line up with items that carry an icon or a check. |
| MenubarItem.disabled | boolean | false | Dims the item in its place, reachable but not chosen — Redo with nothing to redo. |
| MenubarCheckboxItem.checked / onCheckedChange | boolean / (checked: boolean) => void | — | A setting that is on or off; the menu stays open for the next one. |
| MenubarRadioItem.closeOnClick | boolean | false | Radio items keep the menu open; pass it when the choice ends the visit. |
Dependencies
Source
"use client"
import * as React from "react"
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
import { Menubar as MenubarPrimitive } from "@base-ui/react/menubar"
import { cn } from "@/lib/utils"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { CheckIcon } from "lucide-react"
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-[3px]",
className
)}
{...props}
/>
)
}
function MenubarMenu({ ...props }: React.ComponentProps<typeof DropdownMenu>) {
return <DropdownMenu data-slot="menubar-menu" {...props} />
}
function MenubarGroup({
...props
}: React.ComponentProps<typeof DropdownMenuGroup>) {
return <DropdownMenuGroup data-slot="menubar-group" {...props} />
}
function MenubarPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPortal>) {
return <DropdownMenuPortal data-slot="menubar-portal" {...props} />
}
function MenubarTrigger({
className,
...props
}: React.ComponentProps<typeof DropdownMenuTrigger>) {
return (
<DropdownMenuTrigger
data-slot="menubar-trigger"
className={cn(
// The kit's one focus treatment, where the trigger had none: a
// keyboard walking the bar with its menus shut saw nothing move.
"flex items-center rounded-sm px-1.5 py-[2px] text-sm font-medium focus-ring select-none hover:bg-muted aria-expanded:bg-muted",
className
)}
{...props}
/>
)
}
function MenubarContent({
className,
align = "start",
alignOffset = -4,
sideOffset = 8,
...props
}: React.ComponentProps<typeof DropdownMenuContent>) {
return (
<DropdownMenuContent
data-slot="menubar-content"
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn("min-w-36 elev-1 p-1 text-popover-foreground data-[side=bottom]:slide-in-from-top-1 data-[side=inline-end]:slide-in-from-start-1 data-[side=inline-start]:slide-in-from-end-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 data-open:animate-in data-open:fade-in-0 data-open:duration-(--duration-base) data-open:ease-(--ease-standard)", className )}
{...props}
/>
)
}
function MenubarItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuItem>) {
return (
<DropdownMenuItem
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-label focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:ps-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive!",
className
)}
{...props}
/>
)
}
function MenubarCheckboxItem({
className,
children,
checked,
inset,
...props
}: MenuPrimitive.CheckboxItem.Props & {
inset?: boolean
}) {
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 ps-7 pe-1.5 text-label outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:ps-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute start-1.5 flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4">
<MenuPrimitive.CheckboxItemIndicator>
<CheckIcon
/>
</MenuPrimitive.CheckboxItemIndicator>
</span>
{children}
</MenuPrimitive.CheckboxItem>
)
}
function MenubarRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuRadioGroup>) {
return <DropdownMenuRadioGroup data-slot="menubar-radio-group" {...props} />
}
function MenubarRadioItem({
className,
children,
inset,
...props
}: MenuPrimitive.RadioItem.Props & {
inset?: boolean
}) {
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 ps-7 pe-1.5 text-label outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:ps-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute start-1.5 flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4">
<MenuPrimitive.RadioItemIndicator>
<CheckIcon
/>
</MenuPrimitive.RadioItemIndicator>
</span>
{children}
</MenuPrimitive.RadioItem>
)
}
function MenubarLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuLabel> & {
inset?: boolean
}) {
return (
<DropdownMenuLabel
data-slot="menubar-label"
data-inset={inset}
className={cn(
"px-1.5 py-1 text-sm font-medium data-inset:ps-7",
className
)}
{...props}
/>
)
}
function MenubarSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuSeparator>) {
return (
<DropdownMenuSeparator
data-slot="menubar-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function MenubarShortcut({
className,
...props
}: React.ComponentProps<typeof DropdownMenuShortcut>) {
return (
<DropdownMenuShortcut
data-slot="menubar-shortcut"
className={cn(
"ms-auto text-xs tracking-widest text-muted-foreground group-focus/menubar-item:text-accent-foreground",
className
)}
{...props}
/>
)
}
function MenubarSub({
...props
}: React.ComponentProps<typeof DropdownMenuSub>) {
return <DropdownMenuSub data-slot="menubar-sub" {...props} />
}
function MenubarSubTrigger({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuSubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuSubTrigger
data-slot="menubar-sub-trigger"
data-inset={inset}
className={cn(
"gap-1.5 rounded-md px-1.5 py-1 text-label focus:bg-accent focus:text-accent-foreground data-inset:ps-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 DropdownMenuSubContent>) {
return (
<DropdownMenuSubContent
data-slot="menubar-sub-content"
className={cn("min-w-32 elev-1 p-1 text-popover-foreground data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 data-open:animate-in data-open:fade-in-0 data-open:duration-(--duration-base) data-open:ease-(--ease-standard) data-closed:animate-out data-closed:fade-out-0 data-closed:duration-(--duration-fast) data-closed:ease-(--ease-exit)", className )}
{...props}
/>
)
}
export {
Menubar,
MenubarPortal,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarGroup,
MenubarSeparator,
MenubarLabel,
MenubarItem,
MenubarShortcut,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSub,
MenubarSubTrigger,
MenubarSubContent,
}