Menubar
A desktop-style menu bar with danger items, checkbox items, radio groups, submenus and shortcuts.
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarShortcut,
MenubarTrigger,
} from "@/components/ui/menubar"
export default function MenubarDemo() {
return (
<Menubar>
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarItem>
New file <MenubarShortcut>Cmd N</MenubarShortcut>
</MenubarItem>
<MenubarItem>
Open <MenubarShortcut>Cmd O</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem>
Save <MenubarShortcut>Cmd S</MenubarShortcut>
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Edit</MenubarTrigger>
<MenubarContent>
<MenubarItem>Undo</MenubarItem>
<MenubarItem>Redo</MenubarItem>
<MenubarSeparator />
<MenubarItem variant="danger">Delete selection</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>View</MenubarTrigger>
<MenubarContent>
<MenubarItem>Zoom in</MenubarItem>
<MenubarItem>Zoom out</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>
)
}Installation
Run the following command
npx shadcn@latest add @ai2/menubarDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install radix-ui@^1.6.1 lucide-react@^1.23.0 tw-animate-css@^1.4.0Add the cn util
lib/utils.tsimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/* Adds a source-attribution ref param to a URL (the inspiration exports mark their
outbound links with an ai2.design attribution). An invalid URL is returned as is.
This file is SHOWN TO THE CONSUMER: the docs component pages render the source of
`cn` in a code block, so a Turkish comment here would reach every one of those
pages. Keep it English. */
export function withRef(url: string, ref = "ai2.design"): string {
try {
const u = new URL(url)
u.searchParams.set("ref", ref)
return u.toString()
} catch {
return url
}
}Copy the source code
components/ui/menubar.tsx"use client"
import type * as React from "react"
import { Check, ChevronRight, Circle } from "lucide-react"
import { Menubar as MenubarPrimitive } from "@/components/ui/primitives"
import { cn } from "@/lib/utils"
function Menubar({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
return (
<MenubarPrimitive.Root
data-slot="menubar"
className={cn(
"flex h-9 items-center gap-1 rounded-lg border border-border bg-background p-1 shadow-xs",
className
)}
{...props}
/>
)
}
function MenubarMenu(props: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />
}
function MenubarGroup(
props: React.ComponentProps<typeof MenubarPrimitive.Group>
) {
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />
}
function MenubarPortal(
props: React.ComponentProps<typeof MenubarPrimitive.Portal>
) {
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />
}
function MenubarRadioGroup(
props: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>
) {
return (
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
)
}
function MenubarTrigger({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
return (
<MenubarPrimitive.Trigger
data-slot="menubar-trigger"
className={cn(
"flex select-none items-center rounded-md px-2 py-1 text-sm font-medium outline-none transition-colors duration-(--motion-fast) focus:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50 focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className
)}
{...props}
/>
)
}
const menuItemBase =
"relative flex cursor-default select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none transition-colors duration-(--motion-fast) focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_i]:pointer-events-none [&_i]:shrink-0 [&_i]:text-base [&_i]:leading-none [&_i:not([class*='text-'])]:text-muted-foreground"
function MenubarContent({
className,
align = "start",
alignOffset = -4,
sideOffset = 8,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
return (
<MenubarPortal>
<MenubarPrimitive.Content
data-slot="menubar-content"
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 max-w-[calc(100vw-2rem)] min-w-48 origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-md 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-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 motion-reduce:animate-none",
className
)}
{...props}
/>
</MenubarPortal>
)
}
function MenubarItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
inset?: boolean
variant?: "default" | "danger"
}) {
return (
<MenubarPrimitive.Item
data-slot="menubar-item"
data-inset={inset}
data-variant={variant}
className={cn(
menuItemBase,
"data-[inset]:ps-8 data-[variant=danger]:text-danger data-[variant=danger]:focus:bg-danger-soft data-[variant=danger]:focus:text-danger-soft-foreground data-[variant=danger]:*:[svg]:text-danger data-[variant=danger]:*:[i]:text-danger",
className
)}
{...props}
/>
)
}
function MenubarCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
return (
<MenubarPrimitive.CheckboxItem
data-slot="menubar-checkbox-item"
className={cn(menuItemBase, "py-1.5 ps-8 pe-2", className)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="size-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
)
}
function MenubarRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
return (
<MenubarPrimitive.RadioItem
data-slot="menubar-radio-item"
className={cn(menuItemBase, "py-1.5 ps-8 pe-2", className)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="size-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
)
}
function MenubarLabel({
className,
inset,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
inset?: boolean
}) {
return (
<MenubarPrimitive.Label
data-slot="menubar-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:ps-8",
className
)}
{...props}
/>
)
}
function MenubarSeparator({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
return (
<MenubarPrimitive.Separator
data-slot="menubar-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function MenubarShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="menubar-shortcut"
className={cn(
"ms-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
function MenubarSub(props: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
}
function MenubarSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<MenubarPrimitive.SubTrigger
data-slot="menubar-sub-trigger"
data-inset={inset}
className={cn(
menuItemBase,
"data-[inset]:ps-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className
)}
{...props}
>
{children}
<ChevronRight className="ms-auto" />
</MenubarPrimitive.SubTrigger>
)
}
function MenubarSubContent({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
return (
<MenubarPrimitive.SubContent
data-slot="menubar-sub-content"
className={cn(
"z-50 max-w-[calc(100vw-2rem)] min-w-32 origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-lg 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-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 motion-reduce:animate-none",
className
)}
{...props}
/>
)
}
export {
Menubar,
MenubarPortal,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarGroup,
MenubarSeparator,
MenubarLabel,
MenubarItem,
MenubarShortcut,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSub,
MenubarSubTrigger,
MenubarSubContent,
}Manual installs skip the @ai2/tokens theme, so add the token CSS from the theming guide or the tone colors will be missing.
Usage
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarShortcut,
MenubarTrigger,
} from "@/components/ui/menubar"
<Menubar>
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarItem>
New file <MenubarShortcut>Cmd N</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem variant="danger">Delete</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>Wrap the whole row in Menubar, then add one MenubarMenu per top-level entry. Once one menu is open, the pointer and arrow keys move across the bar.
Examples
Multiple menus
A File / Edit / View bar. Open one menu, then move across the row with the pointer or the arrow keys to switch menus. The Edit menu closes with a variant="danger" item.
Submenu
Checkbox and radio items
Checkbox items toggle independently with checked and onCheckedChange; the radio group gives an exclusive choice via value and onValueChange.
Props
All parts forward their radix Menubar props: onSelect and disabled on items, value / onValueChange on the root for controlled open state. The ai2-specific props:
MenubarItem props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "danger" | "default" | danger colors the item (and its icons) with the danger tone for destructive actions. |
inset | boolean | - | Adds left padding so plain items align with checkbox and radio items. Also available on MenubarLabel and MenubarSubTrigger. |
MenubarContent props
| Prop | Type | Default | Description |
|---|---|---|---|
align | "start" | "center" | "end" | "start" | Alignment of the menu against its trigger. |
alignOffset | number | -4 | Offset in pixels along the alignment axis. |
sideOffset | number | 8 | Distance in pixels between the trigger and the menu. |
ai2 Menubar: a desktop application menu bar for React
The ai2 Menubar is a shadcn-compatible menu bar component for React, built on the radix-ui Menubar primitive and styled with Tailwind CSS v4. It is the horizontal File / Edit / View bar you know from desktop apps: a row of top-level triggers, each opening a menu that covers the whole vocabulary, plain actions, a danger variant for destructive items, checkbox items, radio groups, nested submenus, labels, separators and shortcut hints.
Because it ships through the shadcn registry format, you install it with one CLI command, an MCP agent, or a copy-paste, and the source lands in your own project. You own the file; there is no runtime dependency on ai2 itself. The live example above is the exact component you get.
What is the ai2 Menubar?
It is a composition around Menubar as the container, with one MenubarMenu per top-level entry, each holding a MenubarTrigger and a MenubarContent. Inside, the item set is the same one you get from the ai2 Dropdown Menu: MenubarItem, MenubarCheckboxItem, MenubarRadioGroup, label, separator, shortcut and the MenubarSub trio for nesting.
On top of the radix API, items add two ai2 props: variant, where danger colors the item and its icons with the danger tone, and inset, which aligns plain items with checkbox and radio rows. Once one menu is open, moving the pointer across the bar or pressing the arrow keys switches to the neighbouring menu, exactly like a native menu bar.
Why use it
- Roving focus across the bar: The radix-ui Menubar primitive coordinates the whole row: open one menu and the next trigger opens on hover or with the arrow keys, so the bar behaves like a desktop menu instead of a set of unrelated dropdowns.
- Accessible by construction: Menu roles, typeahead, focus management and dismissal come from radix: arrow keys move within and between menus, Enter selects, Escape closes and focus returns to the trigger.
- Danger items built in: variant="danger" styles destructive actions like Delete with the danger token, including their icons and a soft danger highlight on focus, no custom classes needed.
- Stateful items included: Checkbox items with check indicators and exclusive radio groups with dot indicators ship in the same file, so appearance toggles and layout pickers need no extra components.
- Agent-readable metadata: The registry item describes its parts and intended use in plain words, so an MCP agent can find, inspect and install it without guessing.
Features
- shadcn registry install: One command adds the component, its dependencies and the @ai2/tokens theme to your project.
- Styled container: The Menubar root ships a bordered, rounded bar with a subtle shadow, so the row reads as one surface without extra layout work.
- Tuned content placement: MenubarContent defaults to align start with an eight-pixel side offset and a small negative align offset, so menus sit flush under their trigger.
- Labels, separators and shortcuts: MenubarLabel titles a section, MenubarSeparator splits groups, and MenubarShortcut right-aligns a keyboard hint inside any item.
- Submenus included: MenubarSub with MenubarSubTrigger and MenubarSubContent gives hover and keyboard-openable nested menus with a chevron indicator.
- Data attributes for styling: Every part exposes data-slot, items expose data-variant, and radix adds data-state and data-disabled, so any state is targetable from CSS.
Production tips
- Keep the top row short: Four to six top-level menus is the desktop convention. If you need more, the surface probably wants a sidebar or a command palette rather than a wider bar.
- Group with separators, not depth: Split related actions inside one menu with a MenubarSeparator before you reach for a submenu. Flat menus are faster to scan than nested ones.
- Put danger items last: Separate destructive actions with a MenubarSeparator and keep them at the bottom with variant="danger", so muscle memory never lands on Delete.
- Menus act, they do not hold forms: Use onSelect for actions and radio items for view preferences. For entering values, a dialog or a dedicated form is the right place, not the menu bar.
- This is a desktop pattern: A menu bar suits editor-style and dashboard apps on wide screens. On small screens, collapse it into a single dropdown or a navigation drawer instead.
Works with the rest of ai2
A menu bar sits at the top of an app shell, so it pairs with a ai2 Button toolbar and ai2 Kbd caps inside its shortcut hints. For a single button-triggered menu with the same item vocabulary, use the ai2 Dropdown Menu, and for a right-click menu use the ai2 Context Menu.
A danger item usually leads to a confirmation, so pair it with the ai2 Dialog. All of these menus share one token source and the same popover surface styling, so a mixed app stays visually consistent.