Navigation

Menu

<oblyx-menu> is a trigger button that opens a role="menu" popup of actions. For content that needs richer interaction than a list of commands (a form, arbitrary controls), use popover instead.

Basic menu

Rename Duplicate Delete
HTMLKotlin
<oblyx-menu label="Actions">
  <oblyx-menu-item>Rename</oblyx-menu-item>
  <oblyx-menu-item>Duplicate</oblyx-menu-item>
  <oblyx-menu-separator></oblyx-menu-separator>
  <oblyx-menu-item disabled>Delete</oblyx-menu-item>
</oblyx-menu>
oblyxMenu(label = "Actions") {
  oblyxMenuItem { +"Rename" }
  oblyxMenuItem { +"Duplicate" }
  oblyxMenuSeparator()
  oblyxMenuItem(disabled = true) { +"Delete" }
}

Composition

<oblyx-menu-item> and <oblyx-menu-separator> are plain light-DOM children, not a data-driven items array: there's no render-prop mode here, only compound elements the caller composes directly, the same as every other Oblyx family. Neither element holds a reference to its parent menu in either direction: an item resolves "which menu am I in" with this.closest('[role="menu"]') at the moment it's needed (a click, a keypress), not once at connect time. That's what makes a standalone <oblyx-menu-item>, rendered by itself with no <oblyx-menu> around it in an HTMX fragment, degrade to a normal clickable row instead of throwing.

Clicking a disabled item does nothing and never reaches a delegated click listener on an ancestor: a plain custom element gets no platform-level disabled behaviour the way a native <button disabled> does, so this component stops the click itself.

Disabled trigger

The whole menu can be disabled too. The popup can never open while set.

Rename Duplicate
HTMLKotlin
<oblyx-menu label="Actions" disabled>
  <oblyx-menu-item>Rename</oblyx-menu-item>
  <oblyx-menu-item>Duplicate</oblyx-menu-item>
</oblyx-menu>
oblyxMenu(label = "Actions", disabled = true) {
  oblyxMenuItem { +"Rename" }
  oblyxMenuItem { +"Duplicate" }
}

Opening and dismissing

A mouse click on the trigger opens the menu with no item focused. From the keyboard, ArrowDown, Enter or Space open it with the first enabled item focused, and ArrowUp opens it with the last enabled item focused: each is a real, immediate DOM focus move once the popup finishes opening, not aria-activedescendant.

The popup carries popover="auto", so a click anywhere outside it, or Escape, closes it natively with no listener of Oblyx's own for either case. Once focus is on an item, though, Escape and Tab are handled explicitly and differently, and the difference matters:

Selecting an item (click, or Enter/Space) closes the menu and refocuses the trigger, the same as Escape. A native light-dismiss triggered by clicking some other focusable element on the page does not refocus the trigger: that click already moved focus somewhere the user chose, and forcing it back would fight that too.

Keyboard navigation

With focus on an item: ArrowDown/ArrowUp move to the next/previous enabled item, clamped at the ends rather than wrapping. Home/End jump to the first/last enabled item. Typing any other single printable character starts typeahead: keystrokes accumulate into a buffer for 500ms and jump focus to the first enabled item whose text starts with it, case-insensitively.

Focus

Menu does not trap focus, deliberately, unlike popover. Once open, real DOM focus moves onto each row as the cursor moves between them (roving focus, not aria-activedescendant), and every <oblyx-menu-item> is tabindex="-1" so Tab can never land on one directly: the only way a row receives focus is this component (or a row's own keydown handler) moving it there explicitly. A menu's keyboard model requires Tab to close the menu and hand focus to whatever comes next on the page, which is the opposite of what a focus trap does, so wrapping the same trap bundle oblyx-popover uses here would actively break the required behaviour rather than just being unnecessary.

Before it's opened, the popup renders nothing visible at all: it relies on the same [popover]:not(:popover-open) { display: none } user-agent default every anchored popup in this family uses, with no author-origin display declaration of its own to fight it.

Positioning

Placement is CSS anchor positioning, the same mechanism popover and tooltip use: no JavaScript measurement, no positioning library. In a browser without it, the menu still opens, closes and responds to every keyboard interaction on this page correctly; only its position relative to the trigger degrades, to a fixed default location rather than anchored below it.

Reference

<oblyx-menu>

AttributeTypeDefaultDescription
labelstring | undefinedTrigger button text, also used as its accessible name, since a menu trigger has no separate label element the way a form control does. Required; omitting it is a defect and is flagged loudly rather than silently rendering an unnamed button.
disabledbooleanfalseDisables the trigger. The popup can never open while set.

<oblyx-menu-item>

AttributeTypeDefaultDescription
disabledbooleanfalseSkipped by focus navigation, typeahead, and click activation. Same contract as oblyx-option[disabled].

<oblyx-menu-separator>

This element declares no attributes.