Menubar
A row of menus that behave as one strip: Tab reaches the bar once, the
arrow keys move between its triggers from there, and after the first click the
rest open on hover. That is the application menu bar every desktop app has along
its title bar, and it is the shape to reach for when a screen has more commands
than a toolbar can hold.
One menu on its own is a Menu — do not put a single
Menu.Root in a bar to get the styling. A row of unrelated buttons that each
open something is a row of menus, not a menubar, and wrapping it in one changes
what the keyboard does.
Import
import { Menu, Menubar } from "@forte-ui/react";Anatomy
Menubar has no parts of its own. The menus inside it are ordinary
Menu components — Base UI's own anatomy — which is what makes every Menu
feature available here without a second set of components to learn:
<Menubar>
<Menu.Root>
<Menu.Trigger>File</Menu.Trigger>
<Menu.Popup>
<Menu.Item>New document</Menu.Item>
</Menu.Popup>
</Menu.Root>
{/* …one Menu.Root per menu */}
</Menubar>A Menu.Root renders no element, so the triggers are the bar's own children —
which is also why a knob set on the bar reaches every trigger in it.
Examples
Variants
plain is the default and draws nothing: the strip is the menus, which is what
an application menu bar under a title bar looks like. contained puts it on a
panel with a hairline, for a page busy enough that a bare row of words would not
read as a bar.
An application bar
Everything a menu can do it can still do here — shortcuts, checkable rows, radio groups, submenus, links, danger rows. Nothing about being in a bar restricts the popup.
Triggers
A <Menu.Trigger> inside a bar drops its standalone button chrome for a flat
strip item. That is the library's whole visual contribution here, and it applies
to the trigger that is still wearing its own chrome: a trigger given render
keeps whatever it was rendered as, so an overflow button at the end of a bar
stays a button.
Vertical
orientation="vertical" turns the bar into a rail, swaps which arrow keys move
between triggers, and reports aria-orientation="vertical". The triggers
stretch to the bar's width and their labels move to the reading edge.
The popups do not follow: a menu still opens below its trigger, which in a
column lands on top of the next one. Give them side="inline-end".
Disabled
disabled on the bar reaches every trigger — Base UI hands it down through
context, so none of them needs the prop. Disable one menu by putting disabled
on its own <Menu.Trigger>.
Accessibility
| Key | Behaviour |
|---|---|
| Tab | Moves into the bar, landing on the trigger the arrow keys last left, and out of it again — the whole bar is one tab stop. |
| Arrow Right then Arrow Left | Move between triggers in a horizontal bar, mirrored under RTL. In a vertical bar, Arrow Right opens the focused menu. |
| Arrow Down then Arrow Up | Move between triggers in a vertical bar. In a horizontal bar, Arrow Down opens the focused menu and highlights its first row. |
| Home | Moves to the first trigger. |
| End | Moves to the last trigger. |
| Enter then Space | Opens the focused menu and highlights its first row. |
| Esc | Closes the open menu and returns focus to its trigger. |
Once a menu is open, the keys belong to the menu — the same set the Menu page documents, including typeahead and the submenu keys. Arrow Left and Arrow Right at the ends of a menu step to the neighbouring one, which is how a menubar is expected to behave.
Base UI renders role="menubar" and applies a roving tabindex across the
triggers, which is what makes the whole bar a single tab stop. The bar wraps by
default; pass loopFocus={false} if the arrows should stop at the ends.
Theming
Every --forte-menubar-* property is declared on the bar's own element, and an
element's own declaration beats an inherited value — so setting one on :root
or on a theme scope has no effect. Set them on the <Menubar> itself, through
className (an unlayered rule beats the library's @layer forte.components,
whatever its specificity) or an inline style.
The trigger- half is the exception that proves the rule, and it is deliberate:
those properties are declared on the bar but read by the triggers, which
reach them by inheritance. That is what lets one declaration retune every menu
in the bar — and a trigger can still step out of it, because its own declaration
beats the one it would inherit.
The popups are not themed from here. They are portalled to <body>, so nothing
declared on the bar is even in their tree — set --forte-menu-* on each
<Menu.Popup>, as the Menu theming section
describes.
| Property | Controls | Default | contained |
|---|---|---|---|
--forte-menubar-gap | Space between adjacent menu triggers | var(--forte-space-1) | |
--forte-menubar-padding | Padding between the strip's edge and the triggers | 0px | var(--forte-space-1) |
--forte-menubar-radius | Corner radius of the strip | var(--forte-radius-control) | |
--forte-menubar-bg | Strip background | transparent | var(--forte-color-panel) |
--forte-menubar-border-width | Width of the hairline around the strip | 0px | 1px |
--forte-menubar-border-color | Colour of that hairline | var(--forte-color-border) | |
--forte-menubar-trigger-height | Height of a menu trigger in the bar | var(--forte-control-h-md) | |
--forte-menubar-trigger-px | Inline padding inside a trigger. One step tighter than a standalone button's: a bar is a row of words, and md padding spaces them like buttons in a toolbar | var(--forte-control-px-sm) | |
--forte-menubar-trigger-radius | Corner radius of a trigger | var(--forte-radius-control) | |
--forte-menubar-trigger-font-size | Trigger font size | var(--forte-font-size-2) | |
--forte-menubar-trigger-font-weight | Trigger font weight | var(--forte-font-weight-medium) | |
--forte-menubar-trigger-fg | Trigger text colour | var(--forte-color-foreground) | |
--forte-menubar-trigger-bg | Trigger background at rest | transparent | |
--forte-menubar-trigger-bg-hover | Trigger background on hover | var(--forte-color-panel-hover) | var(--forte-color-panel-active) |
--forte-menubar-trigger-bg-open | Background of the trigger whose menu is open | var(--forte-color-primary-soft) | |
--forte-menubar-trigger-fg-open | Text colour of the trigger whose menu is open | var(--forte-color-primary-text) |
API reference
Menubar
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | The `<Menu.Root>`s that make up the bar, and any `<Menu.Separator orientation="vertical">` between them. | |
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
disabled | boolean | false | Whether every menu in the bar ignores user interaction. Base UI hands this down to each `<Menu.Trigger>`, so they all report `data-disabled` without being given the prop individually. |
loopFocus | boolean | true | Whether the arrow keys wrap from the last trigger back to the first. |
modal | boolean | true | Whether an open menu locks page scroll and blocks pointer interaction with the rest of the document. On by default, which is what a desktop menu bar does — and what makes clicking anywhere else close the menu rather than act. |
orientation | MenubarOrientation | horizontal | Which way the bar runs. `vertical` is a sidebar of menus, and each one still opens below its trigger — pass `side="inline-end"` to the popups if they should open alongside instead. It is also what Base UI reports as `aria-orientation`, and it decides which arrow keys move between triggers. |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, MenubarState> | Replaces the rendered `<div>` with another element or component — a `<header>` for an application chrome bar, say. Base UI still applies `role="menubar"` and the keyboard behaviour to whatever comes back. | |
variant | MenubarVariant | plain | How loud the bar itself is. `plain` draws nothing — the strip is the menus, which is what an application menu bar sitting under a title bar should look like. `contained` puts it on a panel with a hairline, for a bar that has to read as its own toolbar inside a busier page. |
Everything inside the bar is documented on the Menu page.