Menus & Popovers
Four widgets for content or actions that stay hidden until the user asks for them. MenuButton and
SplitButton reuse the <menu>/<menuitem> vocabulary from the menu
bar; Popover and Expander host an arbitrary child tree.
MenuButton (<menubutton>) and SplitButton (<splitbutton>)
Section titled “MenuButton (<menubutton>) and SplitButton (<splitbutton>)”<menubutton> is a single button that opens a dropdown menu. <splitbutton> fuses two actions
into one control: a primary click action plus a chevron opening a dropdown for secondary actions.
It is AdwSplitButton on GTK, an NSButton with an attached NSMenu on macOS.
Both take <menuitem> children, plus <menu> for a nested submenu. Same elements as
<menubar>, built into an NSMenu/GMenuModel instead of the app’s
main menu:
<menubutton label="Actions" iconName="open-menu"> <menuitem label="Duplicate" onSelect={duplicate} /> <menuitem label="Rename" onSelect={rename} /> <menuitem role="separator" /> <menuitem label="Delete" iconName="edit-delete" onSelect={remove} /></menubutton>
<splitbutton label="Save" iconName="document-save" onClick={save}> <menuitem label="Save As…" onSelect={saveAs} /> <menuitem label="Save a Copy" onSelect={saveCopy} /></splitbutton>| Widget | Props | Events |
|---|---|---|
<menubutton> |
label, iconName (both createAndUpdate) |
none; each <menuitem> fires its own onSelect |
<splitbutton> |
label, iconName (both createAndUpdate) |
clicked → onClick (the primary action; the dropdown’s items fire their own onSelect) |
Popover (<popover>)
Section titled “Popover (<popover>)”An anchored transient surface (GtkPopover, NSPopover) for a small piece of arbitrary content
attached to a trigger. Its child is a full widget tree rather than <menuitem>s, so it holds
anything a <box> could.
const [open, setOpen] = useState(false);
<box orientation="horizontal" spacing={8}> <button label="Open Popover" onClick={() => setOpen(true)} /> <popover open={open} position="bottom" onClosed={() => setOpen(false)}> <box orientation="vertical" spacing={8} style={{ padding: 12 }}> <label text="Popover content" /> <button label="Close" onClick={() => setOpen(false)} /> </box> </popover></box>;| Prop | Type | Applied | Notes |
|---|---|---|---|
open |
bool | createAndUpdate | Controlled. Set it from onClosed when the user dismisses the popover by clicking outside or pressing Escape. |
anchorRef |
ref to any intrinsic | createAndUpdate | The widget to present from. Without it the popover anchors on its tree parent. |
position |
top | bottom | left | right |
createAndUpdate | Default top. |
closed → onClosed fires with no payload.
Anchoring
Section titled “Anchoring”By default a popover presents from its tree parent (gtk_widget_set_parent on GTK,
NSPopover.show(relativeTo:of:) against the parent view on macOS), which is why the example above
puts it in the same <box> as its button. anchorRef names the trigger instead, and then where the
popover sits in the tree decides nothing:
const trigger = useRef<NdNodeRef<"button">>(null);const [open, setOpen] = useState(false);
<button ref={trigger} label="Open" onClick={() => setOpen(true)} />{/* anywhere in the tree, including a createPortal pool */}<popover anchorRef={trigger} open={open} position="bottom" onClosed={() => setOpen(false)}> <label text="Popover content" /></popover>;The ref is a plain host-element ref (Ref<NdNodeRef>), the same handle sendCommand addresses, and
it may point at any intrinsic. Two things follow from React’s commit order:
- React attaches host refs after the commit that mounts them, so a ref to a widget mounting in the
same render still reads
nulland the anchor lands on the next render that updates the popover. Both backends resolve the anchor when the popover presents, not when the prop arrives, so an anchor created later in the same batch still works. - Dropping
anchorRefresets the anchor and the popover falls back to its tree parent. A popover with no tree parent (rendered throughcreatePortalinto the off-window pool) then has nowhere to present, which is whatscripts/popover-anchor-drive.tsasserts.
Under the hood the prop crosses the wire as anchor, the target’s node id; anchorRef is the
reconciler resolving that id for you, so an app never handles node ids itself.
Expander (<expander>)
Section titled “Expander (<expander>)”An inline disclosure widget (AdwExpanderRow-style on GTK, SwiftUI DisclosureGroup on macOS) for
content that stays in the layout flow instead of floating above it like a Popover. The whole label
row is a click target on macOS, matching GTK’s expander.
const [open, setOpen] = useState(false);
<expander label="More options" expanded={open} onToggled={(e) => setOpen(e.checked)}> <box orientation="vertical" spacing={6} style={{ padding: 8 }}> <checkbox label="An option inside the expander" checked={/* ... */} onToggled={/* ... */} /> </box></expander>;| Prop | Type | Applied | Notes |
|---|---|---|---|
label |
string | createAndUpdate | |
expanded |
bool | createAndUpdate | Controlled. Set it from onToggled. |
toggled → onToggled fires { checked }.
See examples/gallery/main.tsx’s Popovers & Menus tab for all four wired together, and the
Widget Reference for the generated prop tables.