Menubar
One glass strip holding several menus, whose triggers flatten inside it.
Installation
npx shadcn@latest add https://liqui.design/r/menubar.jsonInstalling more than one? Register the namespace once in components.json and
drop the URLs:
{
"registries": {
"@liqui-design": "https://liqui.design/r/{name}.json"
}
}npx shadcn@latest add @liqui-design/menubarThis one installs Menu alongside it, and that is not a convenience — the popups are Menu's, re-exported under menubar names.
Usage
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarShortcut,
MenubarTrigger,
} from '@/components/ui/menubar';<Menubar>
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarItem>
New Window
<MenubarShortcut>⌘N</MenubarShortcut>
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Edit</MenubarTrigger>
<MenubarContent>…</MenubarContent>
</MenubarMenu>
</Menubar>Submenus, checkbox items, radio groups, group labels and separators are all
there under Menubar* names. They are the same components the menu ships, so
anything true of a menu item is true here.
Notes
The strip is the lens
A menubar is several controls sharing one box, which is exactly Toggle Group's shape, and it gets Toggle Group's answer rather than Tabs': the strip refracts, once, and the triggers are flat.
The reason is that there is nothing here that travels. Tabs put the lens on the indicator because a tab strip has exactly one thing that moves and moving is how a lens shows itself. In a menubar the state is which menu is open, and an open trigger is a highlight — a wash of accent over glass that is already there. A second lens inside the strip would have nothing behind it but the strip.
So MenubarTrigger keeps its native <button>, unlike
Button and unlike a standalone
menu
trigger. There is no glass anatomy to put inside it, so none of the reasons to
opt out apply.
The popups sit closer
A standalone menu hangs off a glass button and has to clear that button's bezel, or the popup ends up refracting the trigger. Here the anchor is a flat trigger inside the strip, so the gap only has to clear the strip's own edge:
sideOffset = 4; // MenubarContent, against the menu's 8Everything else about the popup is unchanged, including the keep-mounted portal. With three or four menus in a bar that is three or four popups living in the DOM — see the note on Menu if that trade is wrong for your app.
It is a row of menus, not a new component
Menubar supplies the shared behaviour — arrow keys move between menus, and
once one is open, hovering another opens it. The parts are re-exported:
export { MenuItem as MenubarItem } from '@/components/ui/menu';Restyle an item in menu.tsx and the menubar follows. That is the opposite of
the relationship Menu and Context Menu
have, and it is possible here only because Base UI builds the menubar out of
menu roots rather than out of a parallel component.