Skip to main content

Menu

Info:Overlay html svelte astro Success: coverage 25/25

A menu button: a trigger that opens an anchored popup list of actions.

Live demo

live · @xoji/astro

Menu

App menu bar

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.

Drop one in a Toolbar to build a classic app menu bar: a File, Edit, View row of menu buttons.
Use a kebab or gear label for a row-level or panel-level actions menu.
Listen for select and switch on detail.value to run the chosen action; the menu closes itself.

Props

3 props, straight from the manifest.

PropTypeDefaultBindingsDescription
items MenuItem[]
html svelte astro
The action list. Each `MenuItem` has a `label` and optional `value`, `disabled`, and `separator`. A `{ separator: true }` item renders a divider. Passed as a property in the bindings (serialized to JSON for the element).
label string
html svelte astro
The trigger text, also the popup's accessible name (e.g. "File").
open boolean false
html svelte
Reflects (and controls) whether the menu is open.

Appearance

States

expanded

.xoji-menu__trigger[aria-expanded="true"]

The trigger while its menu is open. Takes the selected tint.

item-active

.xoji-menu__item:hover, .xoji-menu__item:focus-visible

The hovered or keyboard-focused action: the accent-tinted row.

item-disabled

.xoji-menu__item[aria-disabled="true"]

A locked action: muted and non-interactive, skipped by arrow navigation.

Anatomy

The named parts that make up the component, with their selectors.

menu

.xoji-menu

The wrapper holding the trigger and its popover.

trigger

.xoji-menu__trigger

The menu button: carries aria-haspopup, aria-expanded, and the popovertarget that opens the popup.

--space-1 --space-3 --font-sans --text-sm --fg-1 --fg-0 --bg-1 --border-thin --line --radius-md --state-hover --state-selected --ring --border-normal --border-thick --duration-fast --ease-standard

popup

.xoji-menu__popup

The role="menu" floating surface, rendered in the top layer via the Popover API so it escapes ancestor clipping.

--space-1 --font-sans --text-sm --surface-overlay --surface-overlay-border --border-thin --radius-md --elevation-3

item

.xoji-menu__item

An action: a role="menuitem" button. Takes the accent tint on hover/focus.

--space-1 --space-2 --space-3 --fg-1 --fg-disabled --accent-bg --accent-text --radius-sm --duration-fast --ease-standard

separator

.xoji-menu__separator

A role="separator" divider rendered for a separator item.

--border-thin --line --space-1 --space-2

Tokens & coverage

What the component consumes, checked live against what the algorithm produces.

Success:fully covered 25/25 consumed tokens produced default register: 276 tokens

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

Builds the WAI-ARIA menu button pattern: the trigger carries aria-haspopup="menu", aria-expanded, and aria-controls; the popup is a role="menu" of role="menuitem" actions named by the trigger label.
From the trigger, Enter / Space / Down open the menu and focus the first action; Up opens and focuses the last.
In the menu, Up/Down move between enabled actions (wrapping), Home/End jump to the first/last, Enter/Space activates the focused action, Tab closes the menu, and Escape closes it and returns focus to the trigger.
Disabled actions carry aria-disabled and are skipped by arrow navigation and not activatable.
A single roving focus keeps the menu a coherent keyboard surface. Clicking outside closes it (the Popover API's light-dismiss); clicking an action activates it.

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} />