PlMenubar
The strip of words at the top of an application (File, Edit, View) each of which opens a menu. It is one thing to a screen reader, and only ever one of its menus is open.
import { PlMenubar, PlMenubarMenu, PlMenuItem } from 'plass-ui';
<PlMenubar>
<PlMenubarMenu label="File">
<PlMenuItem shortcut="Mod+N">New</PlMenuItem>
</PlMenubarMenu>
</PlMenubar>;import 'package:plass_ui/plass_ui.dart';
PlMenubar(
menus: <PlMenubarMenu>[
PlMenubarMenu(
label: 'File',
items: <PlMenuEntry>[PlMenuItem(label: 'New', shortcut: '⌘N')],
),
],
);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The strip's height and type scale — a rung below the control ladder at every step |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. It reaches the hover, the open menu and the focus rings; the bar draws nothing |
| densityshared | 'default' | 'compact' | 'default' | The padding beside each word. Even the default uses the compact track: a strip is not a row of buttons |
| orientationshared | 'horizontal' | 'vertical' | 'horizontal' | Which way the bar runs. The arrow keys follow it |
| modal | boolean | true | Whether an open menu takes the page away. On, an open menu is what the pointer is talking to |
| loopFocus | boolean | true | Whether the arrow keys wrap around at the ends of the bar |
| disabled | boolean | false | Disables every menu on the bar at once |
| children | ReactNode | — | The menus |
| Prop | Type | Default | Description |
|---|---|---|---|
| menus * | List<PlMenubarMenu> | — | The menus — data here rather than composed children |
| orientationshared | PlassOrientation | PlassOrientation.horizontal | Which way the bar runs |
| disabled | bool | false | Disables every menu on the bar at once |
| sizeshared | PlassSize | PlassSize.md | The strip's height and type scale — a rung below the control ladder at every step |
| colorshared | PlassColor | PlassColor.primary | Semantic colour role. It reaches the hover, the open menu and the focus rings; the bar draws nothing |
| densityshared | PlassDensity | PlassDensity.standard | The padding beside each word. Even the default uses the compact track: a strip is not a row of buttons |
| semanticLabel | String? | — | The name a screen reader gives the bar |
Every native <div> attribute passes straight through. color is excluded because it is a Plass prop here.
PlMenubarMenu
| Prop | Type | Default | Description |
|---|---|---|---|
| label * | ReactNode | — | The word on the bar |
| startIcon | ReactNode | — | Content before the label |
| disabled | boolean | false | Unavailable. The word stays on the bar and opens nothing |
| children | ReactNode | — | The rows, written exactly as they are inside a PlMenu |
| className | string | — | Classes on the word this menu is opened by, alongside the component's own rather than in place of them |
| style | CSSProperties | — | Inline styles on the word this menu is opened by, applied over the custom properties it wrote |
| Prop | Type | Default | Description |
|---|---|---|---|
| label * | String | — | The word on the bar |
| items * | List<PlMenuEntry> | — | The rows, written exactly as they are inside a PlMenu |
| startIcon | Widget? | — | Content before the label |
| disabled | bool | false | Unavailable. The word stays on the bar and opens nothing |
What the shared axes mean across the library is in prop conventions.
Composition
Not the look. A row of separate PlMenus would look the same and differ in the ways that matter: it would be a row of buttons to a screen reader rather than one menubar whose words are menuitems, and nothing would keep two of them from being open at once.
On a bar, crossing the strip also walks through the menus and the arrow keys move between them as well as inside them. Base UI owns all of it.
Shared with PlMenu
A PlMenubarMenu takes the rows a PlMenu takes (PlMenuItem, PlMenuSeparator, PlMenuGroup, PlMenuSubmenu, PlMenuCheckboxItem, PlMenuRadioItem), because it is the same menu with a different trigger.
What it does not take is size, color or density. Those belong to the bar: they are the one place the axes can be set once and hold for every menu on the strip, and a bar whose third menu is a size out is not a bar.
import { useState } from 'react';
import {
PlMenubar,
PlMenubarMenu,
PlMenuCheckboxItem,
PlMenuItem,
PlMenuSeparator,
PlMenuSubmenu
} from 'plass-ui';
export default function MenubarRows() {
const [grid, setGrid] = useState(true);
const [rulers, setRulers] = useState(false);
return (
<PlMenubar>
<PlMenubarMenu label="View">
<PlMenuCheckboxItem checked={grid} onCheckedChange={setGrid}>
Grid
</PlMenuCheckboxItem>
<PlMenuCheckboxItem checked={rulers} onCheckedChange={setRulers}>
Rulers
</PlMenuCheckboxItem>
<PlMenuSeparator />
<PlMenuSubmenu label="Appearance">
<PlMenuItem>Light</PlMenuItem>
<PlMenuItem>Dark</PlMenuItem>
<PlMenuItem>System</PlMenuItem>
</PlMenuSubmenu>
</PlMenubarMenu>
<PlMenubarMenu label="Help">
<PlMenuItem href="#">Documentation</PlMenuItem>
<PlMenuItem>About</PlMenuItem>
</PlMenubarMenu>
</PlMenubar>
);
}Examples
size
The strip sits a rung below the control ladder at every step, and it takes the compact padding track even at density="default".
Both are the same decision: a menu bar is a strip of words, and it is usually inside something that already has a height, a PlToolbar, a PlHeader. Sized as controls, File Edit View would be three buttons in a row and would make the bar taller than the thing it is drawn on.
import { PlMenubar, PlMenubarMenu, PlMenuItem, type PlassSize } from 'plass-ui';
export default function MenubarSizes() {
return (
<div className="flex flex-col gap-4">
{(['xs', 'sm', 'md', 'lg', 'xl'] as PlassSize[]).map((size) => (
<PlMenubar key={size} size={size}>
<PlMenubarMenu label={size}>
<PlMenuItem>New</PlMenuItem>
</PlMenubarMenu>
<PlMenubarMenu label="Edit">
<PlMenuItem>Copy</PlMenuItem>
</PlMenubarMenu>
</PlMenubar>
))}
</div>
);
}orientation
vertical is the shape a side rail of menus takes.
import { PlBox, PlMenubar, PlMenubarMenu, PlMenuItem } from 'plass-ui';
export default function MenubarOrientation() {
return (
<PlBox size="sm" className="w-44">
<PlMenubar orientation="vertical" size="sm">
<PlMenubarMenu label="File">
<PlMenuItem>New</PlMenuItem>
<PlMenuItem>Open…</PlMenuItem>
</PlMenubarMenu>
<PlMenubarMenu label="Edit">
<PlMenuItem>Undo</PlMenuItem>
</PlMenubarMenu>
<PlMenubarMenu label="Help">
<PlMenuItem>About</PlMenuItem>
</PlMenubarMenu>
</PlMenubar>
</PlBox>
);
}No surface
A menu bar sits on something, and a sheet under a strip that is already on a sheet is two sheets. The bar contributes a flex row and four colour slots, and nothing else.
The open menu is marked in colour and nothing else. The word does not move and the strip does not change height, which is the same rule every control in the library follows under a pointer.
Differences from the React build
| React | Flutter | Why |
|---|---|---|
composed PlMenubarMenu children | menus: List<PlMenubarMenu> as data | A menu on a bar is a word and a list of rows, and a list is what the strip can count. |
| crossing the strip walks through the menus | pressing elsewhere puts the open one away | An open menu's dismiss layer is between the pointer and the strip, so the words never hear it arrive. Only one is ever open either way. |
modal, loopFocus | — | There is no page to make inert and no arrow-key ring to wrap: an open menu already owns the pointer. |
aria-expanded on the word | SemanticsRole.menuItem with expanded | The same state under the framework's own name. Once a menu is open it is the accessibility tree, so the strip says which one in colour as well. |
className, style, native attributes | — | There is no class list and no style attribute to pass through. |
Accessibility
- The strip is a real
menubarand each word amenuitemthat reports whether it is expanded. - The focus ring is turned inward on a word, because a strip's items are a hair apart and a ring drawn outside one would overlap its neighbours.
- A
disabledmenu keeps its word on the bar and opens nothing.disabledon the bar does it to every menu at once.
- The arrow keys move along the bar and into an open menu; Esc closes it and returns focus to its word.
loopFocusdecides whether the bar wraps at its ends. modalis on by default, so an open menu is what the pointer is talking to. The page behind it is inert until it closes.