Skip to content

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.

menubar/basic.tsx

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.

menubar/variants.tsx

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.

menubar/application.tsx

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.

menubar/triggers.tsx

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".

menubar/orientation.tsx

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>.

menubar/disabled.tsx

Accessibility

Keyboard interactions
KeyBehaviour
TabMoves 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 LeftMove between triggers in a horizontal bar, mirrored under RTL. In a vertical bar, Arrow Right opens the focused menu.
Arrow Down then Arrow UpMove between triggers in a vertical bar. In a horizontal bar, Arrow Down opens the focused menu and highlights its first row.
HomeMoves to the first trigger.
EndMoves to the last trigger.
Enter then SpaceOpens the focused menu and highlights its first row.
EscCloses 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.

menubar/theming.tsx

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.

Theming tokens for Menubar
PropertyControlsDefaultcontained
--forte-menubar-gapSpace between adjacent menu triggersvar(--forte-space-1)
--forte-menubar-paddingPadding between the strip's edge and the triggers0pxvar(--forte-space-1)
--forte-menubar-radiusCorner radius of the stripvar(--forte-radius-control)
--forte-menubar-bgStrip backgroundtransparentvar(--forte-color-panel)
--forte-menubar-border-widthWidth of the hairline around the strip0px1px
--forte-menubar-border-colorColour of that hairlinevar(--forte-color-border)
--forte-menubar-trigger-heightHeight of a menu trigger in the barvar(--forte-control-h-md)
--forte-menubar-trigger-pxInline 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 toolbarvar(--forte-control-px-sm)
--forte-menubar-trigger-radiusCorner radius of a triggervar(--forte-radius-control)
--forte-menubar-trigger-font-sizeTrigger font sizevar(--forte-font-size-2)
--forte-menubar-trigger-font-weightTrigger font weightvar(--forte-font-weight-medium)
--forte-menubar-trigger-fgTrigger text colourvar(--forte-color-foreground)
--forte-menubar-trigger-bgTrigger background at resttransparent
--forte-menubar-trigger-bg-hoverTrigger background on hovervar(--forte-color-panel-hover)var(--forte-color-panel-active)
--forte-menubar-trigger-bg-openBackground of the trigger whose menu is openvar(--forte-color-primary-soft)
--forte-menubar-trigger-fg-openText colour of the trigger whose menu is openvar(--forte-color-primary-text)

API reference

Props for Menubar
PropTypeDefaultDescription
childrenReactNodeThe `<Menu.Root>`s that make up the bar, and any `<Menu.Separator orientation="vertical">` between them.
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
disabledbooleanfalseWhether 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.
loopFocusbooleantrueWhether the arrow keys wrap from the last trigger back to the first.
modalbooleantrueWhether 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.
orientationMenubarOrientationhorizontalWhich 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.
renderReactElement<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.
variantMenubarVariantplainHow 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.