Skip to content
View as Markdown

Disclosures

Menu

A list of actions opened from a trigger: the ARIA Authoring Practices Guide (APG) menu-button pattern on top of the browser's top layer and anchor positioning.

Import

TSX
import { Menu } from "@loamui/core";

Usage

Every example has a CSS tab. That’s the real, complete stylesheet for the component: plain, static CSS, with nothing running in the browser.

Basic usage

Compose the menu from parts and wire each item's onClick to its action. The popup uses the same engine as Popover (native popover attribute and anchor positioning where supported, a wrapper-anchored fallback elsewhere) with the APG menu-button keyboard pattern on top: arrow keys rove focus, typing jumps to a matching item, and activating one closes the menu and returns focus to the trigger.

Groups and separators

Group related items under a label with Menu.Group and Menu.GroupLabel; Menu.Separator is a real <hr> between them.

Destructive group

Contextual meaning is a custom property: declare --loam-context: danger on a Menu.Group and the items inside adopt the danger accent, no props involved. Put Delete and its kin in their own labelled group so the separation and the colour both signal the stakes before the click.

Checkable items

A setting that lives in the menu is a CheckboxItem (on/off, role menuitemcheckbox) or a RadioGroup of RadioItems (one of a set, role menuitemradio). Both keep the menu open on activation, since a setting is usually one of several adjusted in one visit; the glyph is drawn by CSS from aria-checked.

Disabled items

Disabled items use aria-disabled, so they stay visible to assistive technology but are skipped by roving focus (one tab stop, arrow keys move between items) and cannot be activated.

Guidance

When to use it

  • For a short list of actions on an object (rename, duplicate, export, delete) collapsed behind a single trigger.
  • When the actions are secondary enough that laying them all out as visible buttons would clutter the surface.

When not to

  • For choosing a value that persists, use Select, which has real selection semantics the menu role does not promise.
  • For navigation, prefer visible links; a menu hides destinations users need behind an extra interaction.
  • For one or two actions: plain Buttons are simpler and one click fewer.

Accessibility

  • Implements the APG menu-button pattern: ArrowDown/ArrowUp on the trigger open the menu and focus the first/last item; inside, arrow keys rove focus through the items (looping), Home/End jump to the ends, and typing jumps to the next item matching the query.
  • Escape closes and returns focus to the trigger, as does activating an item; Tab closes the menu and lets focus continue naturally: the menu moves focus, it never traps it.
  • The trigger is a real <button> with aria-haspopup="menu" and aria-expanded; the popup is role="menu" with role="menuitem" children, and Menu.Separator is a real <hr>: the platform's separator role, no ARIA needed.
  • Disabled items use aria-disabled rather than disabled, so they remain visible to assistive technology while roving focus skips them.
  • Where the popover attribute and anchor positioning are both supported, the browser provides top-layer rendering, light dismiss and Escape; other browsers get a wrapper-anchored fallback with the same behaviour re-implemented, the deliberate no-polyfill trade-off, per the browser support policy in CONTRIBUTING.

How it works

Disabled items stay in the menu

A disabled item renders aria-disabled and is skipped by roving focus but stays visible and announced: the user learns the command exists and is currently unavailable. Removing it instead teaches them the feature is gone.

Destructive commands live in a labelled danger group

Put Delete and its kin in a Menu.Group with a GroupLabel, inside a --loam-context: danger wrapper. The separation and the colour both signal the stakes before the click, and the group label is announced with each item.

Icon-only triggers need a name

The ⋯ trigger reads as "menu" to a sighted user and as nothing to anyone else. Give it an aria-label naming the object it operates on ("Actions for INV-1024", not "menu"), because in a list of rows, ten triggers labelled "menu" are indistinguishable.

Parts

Custom properties

PropertySyntaxDefaultDescription
--loam-menu-sizeCSS length18remThe list's widest extent; content sizes it between a floor derived from this and the cap. Never wider than the viewport.