Menu Bar
A <menubar> is the clearest demonstration of the design-language philosophy (see
App Model): the same declared tree adopts each platform’s own idiom for
where the app’s commands live, rather than drawing one widget identically everywhere. On macOS
that’s a real menu bar; on GNOME it’s a primary hamburger menu.
<menubar> is a <window> child, a sibling of the window’s content, not nested inside it. It
contains <menu label> entries, which in turn contain <menuitem> entries:
<window title="ND Notes"> <menubar> <menu label="File"> <menuitem label="New Note" iconName="document-new" accelerator="primary+n" onSelect={createNote} /> </menu> <menu label="Note"> <menuitem label="Pin" accelerator="primary+p" onSelect={togglePin} /> <menuitem role="separator" /> <menuitem label="Delete" iconName="edit-delete" accelerator="primary+backspace" onSelect={deleteNote} /> </menu> </menubar> <splitview>{/* … */}</splitview></window>(Adapted from examples/notes/menubar-probe.tsx, the headless acceptance fixture for this
machinery.)
MenuItem: role or onSelect, never both in effect
Section titled “MenuItem: role or onSelect, never both in effect”A <menuitem> has either a role, meaning platform-provided behavior, or an onSelect handler for
custom behavior. If both are set, onSelect wins and the role contributes nothing beyond
documentation. role="separator" renders a native separator, and label, iconName, and
accelerator are ignored on it.
The full prop set (the Widget Reference has the generated table):
| Prop | Meaning |
|---|---|
label |
The item’s text. |
iconName |
A freedesktop name, the same vocabulary as Button.iconName (see Icons), resolved through the same SF Symbol map on macOS. Deliberately not rendered on GNOME, where popover menus have no item icons and the HIG discourages them. |
accelerator |
Grammar mod+…+key. Mods are primary, shift, alt, ctrl, where primary is ⌘ on macOS and Ctrl on GNOME. The key is one printable character or a named key: enter, escape, backspace, delete, space, tab, f1 through f12, left, right, up, down, comma, period. |
role |
none by default, plus a fixed vocabulary: separator, about, settings, quit, undo, redo, cut, copy, paste, delete, selectAll, close, minimize, zoom, fullscreen. |
enabled |
createAndUpdate. A disabled item does not fire onSelect on either platform. |
onSelect |
Fires the selected event. |
Platform rendering
Section titled “Platform rendering”macOS: a real main menu, with or without a declared <menubar>
Section titled “macOS: a real main menu, with or without a declared <menubar>”macOS installs the full standard main menu (App, File, Edit, View, Window, Help, each wired to the
responder chain via NSText selectors and NSApplication actions) at window creation, even when
the tree has no <menubar> at all. That’s why Edit > Copy, Cut, Paste, Select All, Quit, Minimize, and friends work in every text field with no app code: they’re
responder-chain selectors with a nil target, not custom handlers.
A declared <menu label> is merged into that default chrome:
- A label that matches a default top-level title (
File,Edit,View,Window,Help) gets its items appended to that menu after a separator. - Any other label becomes a new top-level menu, inserted before Window (after View) in declaration order.
<menubar defaults={false}> opts out of the merge: only the App menu (About/Hide/Quit) plus your
declared menus are installed. The default File/Edit/View/Window/Help menus are not.
GNOME: no top menubar, by design
Section titled “GNOME: no top menubar, by design”GNOME Shell doesn’t show a top-level menubar; that’s correct GNOME design. Instead, <menubar>
renders as a primary menu button (GtkMenuButton, open-menu-symbolic,
tooltip “Main Menu”) homed in the last headerbar in document order (by GNOME convention, the
content pane’s header). If the tree has no headerbar at all, it falls back to
gtk.Application.setMenubar, an in-window menu strip.
Roles that would duplicate window or system chrome are dropped per the GNOME HIG: quit,
close, minimize, undo, cut, copy, paste, delete, selectAll, zoom, fullscreen. A
menu whose items all drop is omitted entirely. The two roles that do survive get GNOME-native
treatment instead of a plain menu item: about opens an AdwAboutDialog, and settings renders as
a “Preferences” item that emits onSelect like any custom item.
Beyond the menu bar
Section titled “Beyond the menu bar”<menu>/<menuitem> aren’t exclusive to <menubar>. The same elements are also the dropdown
content for <menubutton> and <splitbutton>, and for a macOS
<trayitem>’s right-click menu. The prop
set, role/onSelect exclusivity, and accelerator grammar documented above apply identically in all
three places; only the owning widget differs.
Automation
Section titled “Automation”Menu nodes appear in getTree (see Automation Socket)
with labels as their text and nominal (non-visual) bounds, since they’re chrome rather than an
on-screen widget. The click RPC on a menuitem ref dispatches onSelect (or fires the role’s
native action) on both backends, so an automation-driving agent can exercise a File > New Note
command the same way it clicks a button.
Caveats
Section titled “Caveats”iconNameon a<menuitem>has no visible effect on GNOME. This is intentional; don’t rely on the icon for information the label doesn’t already carry.- macOS has one merge rule (append after a separator on a title match, otherwise insert before Window). There’s no way to target Edit/View/Help insertion order beyond that.
roleandonSelectare not additive: setting both silently drops the role’s native behavior in favor of the custom handler, so don’t expect e.g.role="quit"plus anonSelectto run your handler and quit.