Skip to main content

menu-button — dropdown and context menus

UiMenuButton is a button that opens a floating menu of menu-items anchored to itself. Where menu is the persistent sidebar, a menu button is transient: it lives closed, opens on click, and closes on outside-click, Esc or when an entry is chosen.

It is deliberately placeable anywhere a node can go — on its own as a toolbar overflow ("⋮"), or dropped inside another node to become that node's context menu, which is the standard pattern for table and tree rows.

Live — open the kebab on the left (the context-menu shape); its "Delete" entry asks for confirmation first. The labelled "Actions" dropdown on the right has a nested "Move to" submenu. Click outside or press Escape to close.

Fields

FieldTypeMeaning
idStringNode id — also the DOM id and the patch target.
itemsList<UiMenuItem>The entries shown in the popover.
iconStringTrigger glyph token. Defaults to more (a vertical "⋮") when absent.
labelStringOptional trigger text; when set the trigger renders as a labelled button with a caret.
variantICON · BUTTONTrigger look. Defaults to BUTTON when a label is set, else ICON.
alignSTART · ENDWhich edge of the trigger the popover lines up with. START opens rightwards; END (the default) right-aligns it.
cssClassStringExtra CSS class on the wrapper.

Building one

// The kebab: icon-only trigger, a divider, a danger entry with a confirm.
UiMenuButton.of("row-menu",
UiMenuItem.of("ren", "Rename").icon("edit").onClick(UiTrigger.api("POST", "/rename/42")),
UiMenuItem.of("dl", "Download").icon("download").onClick(UiTrigger.download("/files/42")),
UiMenuItem.divider(),
UiMenuItem.of("del", "Delete").icon("delete").danger(true)
.confirm("Delete this file?")
.onClick(UiTrigger.api("DELETE", "/files/42"))
).align(UiMenuButton.Align.END);

// The labelled dropdown, with a nested submenu:
UiMenuButton.of("acts",
UiMenuItem.of("exp", "Export CSV").icon("download").onClick(UiTrigger.go("/export")),
UiMenuItem.group("move", "Move to",
UiMenuItem.of("inbox", "Inbox").onClick(UiTrigger.api("POST", "/move/inbox")),
UiMenuItem.of("archive", "Archive").onClick(UiTrigger.api("POST", "/move/archive"))
).icon("folder"),
UiMenuItem.link("help", "Help", "/help").icon("info")
).label("Actions").icon("grid");

Notes

A menu button in a row is the standard context menu. Because it is just a node, drop one into a table cell or a tree node's labelNode and every row gains its own "⋮" of per-row actions — without widening the row with three separate buttons:

UiTreeNode.of("f1", null).labelNode(UiStack.of(
UiText.of("report.pdf"),
UiMenuButton.of("f1-menu",
UiMenuItem.of("f1-ren", "Rename").icon("edit").onClick(/* … */),
UiMenuItem.divider(),
UiMenuItem.of("f1-del", "Delete").icon("delete").danger(true)
.confirm("Delete report.pdf?").onClick(/* … */)
)).direction(UiStack.Direction.HORIZONTAL).gap(8));

It is never clipped. The popover is rendered inline (so it exists in SSR markup and stays inside the event bus's scope) but positioned with position: fixed at open time. That is what makes the row pattern above work: a scrolling table body or an overflow: hidden app shell cannot cut the menu off. The bus flips it above the trigger when there is no room below, clamps it into the viewport, and flips submenus to the other side near the right edge.

Opening degrades gracefully. The trigger is a native <details>/<summary>, so with no JS at all clicking it still opens and closes the popover and each entry's href still navigates. The bus takes over on top: outside-click and Esc to close, closing after a choice, and dispatching each entry's onClick instead of navigating.

Danger and dividers carry the meaning. A popover is the one place where menu-item's divider and danger earn their keep — separate the destructive entry from the benign ones and colour it. Add the inherited confirm to that entry: the colour warns, the confirm actually guards.

Two shapes, one type. No label means an icon-only kebab (variant: ICON, default glyph more); setting a label promotes it to a labelled dropdown button. align: END is right for a trailing kebab at the end of a row; use START when the trigger sits at the left edge and the menu should open rightwards.

See also

  • menu-item — the entries, and the UiAction fields they inherit.
  • menu — the persistent sidebar counterpart.
  • action — a single control instead of several behind one.
  • table and tree — where per-row context menus belong.
  • Triggers & actions — everything an entry's onClick can do.