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 drives the markup. An action carries a label plus optional value, disabled, and a hint (a trailing muted/mono accelerator like Ctrl+S); a { separator: true } entry renders a role="separator" divider; and a { heading: string } entry opens a labeled role="group" the following actions sit under, so a real app menu can group its commands under "File" and "Help" headers. 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-danger
A destructive action (intent: "danger"): the row reads in the danger ink, and its hover/focus takes the danger tint instead of the accent one, so the one irreversible item stands apart.
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.
item-hint
A row's trailing accelerator hint (e.g. Ctrl+S): a muted, monospaced span at the end of the action, exposed so a consumer can restyle the keycap text.
heading
A group label for a heading item: the muted, uppercase title a labeled section of actions sits under.
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 xtyle-default register
(derive(xtyleDefault, { 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
--danger-bg
--danger-text
--duration-fast
--ease-standard
--elevation-3
--fg-0
--fg-1
--fg-2
--fg-disabled
--font-mono
--font-sans
--leading-normal
--line
--radius-md
--radius-sm
--ring
--space-1
--space-2
--space-3
--state-hover
--state-selected
--surface-overlay
--surface-overlay-border
--text-sm
--text-xs
Accessibility
Code
A File menu
A menu button opening a list with a disabled action and separators between groups.
<xtyle-menu label="File"></xtyle-menu>
<script>
const menu = document.querySelector("xtyle-menu");
menu.items = [
{ heading: "File" },
{ label: "New", value: "new", hint: "Ctrl+N" },
{ label: "Open…", value: "open", hint: "Ctrl+O" },
{ label: "Save", value: "save", hint: "Ctrl+S" },
{ label: "Save As…", value: "save-as", disabled: true },
{ heading: "Help" },
{ label: "Shortcuts", value: "shortcuts", hint: "?" },
{ separator: true },
{ label: "Close", value: "close", intent: "danger" },
];
menu.addEventListener("select", (e) => console.log(e.detail));
</script>
<script lang="ts">
import { Menu } from "@xtyle/svelte";
const items = [
{ heading: "File" },
{ label: "New", value: "new", hint: "Ctrl+N" },
{ label: "Open…", value: "open", hint: "Ctrl+O" },
{ label: "Save", value: "save", hint: "Ctrl+S" },
{ label: "Save As…", value: "save-as", disabled: true },
{ heading: "Help" },
{ label: "Shortcuts", value: "shortcuts", hint: "?" },
{ separator: true },
{ label: "Close", value: "close", intent: "danger" },
];
</script>
<Menu label="File" {items} onselect={(e) => console.log(e.detail)} />
---
import { Menu } from "@xtyle/astro";
const items = [
{ heading: "File" },
{ label: "New", value: "new", hint: "Ctrl+N" },
{ label: "Open…", value: "open", hint: "Ctrl+O" },
{ label: "Save", value: "save", hint: "Ctrl+S" },
{ label: "Save As…", value: "save-as", disabled: true },
{ heading: "Help" },
{ label: "Shortcuts", value: "shortcuts", hint: "?" },
{ separator: true },
{ label: "Close", value: "close", intent: "danger" },
];
---
<Menu label="File" items={items} />