Menu
A menu button: a trigger that opens an anchored popup list of actions.
Live demo
Menu is the app-menu shape: a labeled trigger (a File button, a kebab, a profile name) that opens a floating list of actions under it. It builds the WAI-ARIA menu button pattern: the trigger carries aria-haspopup="menu" and aria-expanded, and the popup is a role="menu" of role="menuitem" actions with a single roving focus, so the keyboard walks it like a native menu.
Like Tree, it is data-driven: an items array of { label, value?, disabled?, separator? } drives the markup, and a separator item renders a role="separator" divider rather than an action. The popup uses the native Popover API, so it renders in the top layer and escapes any clipping or stacking context an ancestor would otherwise impose, positioned under the trigger (and flipped up when there is no room below). Choosing an action fires a select event with the item's value, label, and index and closes the menu; the engine never navigates, the consumer decides what an action does. Its chrome (the overlay surface, the elevation, the accent-tinted active row) is derived, so a menu frames its actions in the theme's own voice.
When to use
How this component composes with the rest of the set.
Props
3 props, straight from the manifest.
| Prop | Type | Default | Bindings | Description |
|---|---|---|---|---|
Appearance
States
expanded
The trigger while its menu is open. Takes the selected tint.
item-active
The hovered or keyboard-focused action: the accent-tinted row.
item-disabled
A locked action: muted and non-interactive, skipped by arrow navigation.
Anatomy
The named parts that make up the component, with their selectors.
menu
The wrapper holding the trigger and its popover.
trigger
The menu button: carries aria-haspopup, aria-expanded, and the popovertarget that opens the popup.
popup
The role="menu" floating surface, rendered in the top layer via the Popover API so it escapes ancestor clipping.
item
An action: a role="menuitem" button. Takes the accent tint on hover/focus.
separator
A role="separator" divider rendered for a separator item.
Tokens & coverage
What the component consumes, checked live against what the algorithm produces.
Live coverage check against the xoji-default register
(derive(xojiDefault, { anchors }) →
coverComponent(manifest, register)). Every token this component
consumes must be a key the algorithm produces.
--accent-bg
--accent-text
--bg-1
--border-normal
--border-thick
--border-thin
--duration-fast
--ease-standard
--elevation-3
--fg-0
--fg-1
--fg-disabled
--font-sans
--line
--radius-md
--radius-sm
--ring
--space-1
--space-2
--space-3
--state-hover
--state-selected
--surface-overlay
--surface-overlay-border
--text-sm
Accessibility
Code
A File menu
A menu button opening a list with a disabled action and separators between groups.
<xoji-menu label="File"></xoji-menu>
<script>
const menu = document.querySelector("xoji-menu");
menu.items = [
{ label: "New", value: "new" },
{ label: "Open…", value: "open" },
{ separator: true },
{ label: "Save", value: "save" },
{ label: "Save As…", value: "save-as", disabled: true },
{ separator: true },
{ label: "Close", value: "close" },
];
menu.addEventListener("select", (e) => console.log(e.detail));
</script>
<script lang="ts">
import { Menu } from "@xoji/svelte";
const items = [
{ label: "New", value: "new" },
{ label: "Open…", value: "open" },
{ separator: true },
{ label: "Save", value: "save" },
{ label: "Save As…", value: "save-as", disabled: true },
{ separator: true },
{ label: "Close", value: "close" },
];
</script>
<Menu label="File" {items} onselect={(e) => console.log(e.detail)} />
---
import { Menu } from "@xoji/astro";
const items = [
{ label: "New", value: "new" },
{ label: "Open…", value: "open" },
{ separator: true },
{ label: "Save", value: "save" },
{ label: "Save As…", value: "save-as", disabled: true },
{ separator: true },
{ label: "Close", value: "close" },
];
---
<Menu label="File" items={items} />