Skip to main content

menu — the collapsible navigation sidebar

UiMenu is the vertical navigation sidebar an admin shell puts beside its content. It holds nestable menu-items and toggles between three display states with a hamburger: expanded (icon + label, groups open inline), rail (a narrow icon-only strip, groups fly out on hover) and hidden (off-canvas).

The state field is only the initial state the server renders. Once the SPA event bus is loaded the hamburger cycles the state client-side and remembers the choice; with no JS at all the items are still real links and the groups still native <details> disclosures.

Live — click the hamburger to cycle expanded → rail → hidden; the content panel reflows as the sidebar narrows. In the rail, hover "Catalog" for its fly-out. Clicking an item patches the page title.

Fields

FieldTypeMeaning
idStringNode id — also the DOM id, the patch target and the localStorage key for the collapse state.
titleStringSidebar heading shown above the items.
itemsList<UiMenuItem>The top-level entries.
stateEXPANDED · RAIL · HIDDENInitial display state. Defaults to EXPANDED.
modePUSH · OVERLAY · RESPONSIVEHow the sidebar relates to the content beside it. Defaults to PUSH.
sideLEFT · RIGHTWhich edge the menu sits on. Defaults to LEFT.
toggleBooleanWhether to render the menu's own hamburger. Defaults to true; set false when a header owns it.
cssClassStringExtra CSS class on the <nav>.

Building one

UiMenu.of("nav", "Admin",
UiMenuItem.link("dash", "Dashboard", "/dash").icon("grid").selected(true),
UiMenuItem.group("catalog", "Catalog",
UiMenuItem.link("prod", "Products", "/products").icon("document"),
UiMenuItem.link("cust", "Customers", "/customers").icon("show")
).icon("folder").open(true),
UiMenuItem.link("orders", "Orders", "/orders").icon("download").badge("12"),
UiMenuItem.of("reload", "Reload").icon("add")
.onClick(UiTrigger.api("POST", "/reload")) // dispatches instead of navigating
).state(UiMenu.State.EXPANDED)
.mode(UiMenu.Mode.RESPONSIVE)
.side(UiMenu.Side.LEFT);

Notes

The three states cost no round-trip. The hamburger is handled entirely by the event bus: it reads the menu's current state, computes the next one and swaps a CSS class, then persists the choice in localStorage under the menu's id. The bus re-applies that stored state after every render, so a user's collapse choice survives reloads and patches without any server involvement. The server can still drive it explicitly — render or REPLACE the menu with a different state and it appears that way.

mode decides how the content reacts. PUSH (the default) gives the menu real layout space, so the content beside it reflows wider as the menu collapses — place the menu next to the content in a horizontal stack. OVERLAY turns it into a drawer floating over the content with a dimmed backdrop that closes on click; its container needs position: relative. RESPONSIVE is the usual admin-shell behaviour: push on a wide screen (the hamburger flips expanded ⇄ rail, so the sidebar never fully vanishes), an overlay drawer below max-width: 768px (closed by default). See responsive layout.

The hamburger can live in the header. Set UiHeader.menuToggle to the menu's id and turn the menu's own toggle off with .toggle(false). The whole app shell is then just composition — a header on top, [menu, content] in a horizontal stack:

UiStack.of(
UiHeader.of("Acme Admin").menuToggle("nav"),
UiStack.of(
UiMenu.of("nav", "Acme", items).mode(UiMenu.Mode.RESPONSIVE).toggle(false),
contentPanel
).direction(UiStack.Direction.HORIZONTAL).withCssClass("app-body")
).direction(UiStack.Direction.VERTICAL);
.app-body { position: relative; overflow: hidden; align-items: stretch; }

Every item is patch-addressable. Each entry is a full UiNode of type menu-item, so a patch can REPLACE a single one — flip its selected, swap a label, update a badge count — without re-rendering the sidebar and losing its scroll position or open groups.

Widths are CSS variables. Override them per app rather than per node:

.sui-menu { --sui-menu-w: 260px; --sui-menu-rail-w: 56px; }

See also