Overlay · astro-ui · MenuNew

MenuJS

Floating action menu with the WAI-ARIA menu-button keyboard model.

Menu is the “⋯” action list: a trigger button that opens a role="menu" panel of actions. MenuItem renders a real <button> or <a>, so activation, middle-click, and copy-link all behave like normal HTML. The scoped script adds the parts native HTML cannot express — roving focus, arrow/Home/End navigation, typeahead, Escape-to-close with focus return, and outside-click dismissal.

Features

  • WAI-ARIA menu-button pattern: aria-haspopup, aria-expanded, role=menu
  • Arrow keys with wraparound, Home/End, letter typeahead
  • Escape closes and returns focus to the trigger
  • Outside pointerdown and Tab close without stealing focus
  • Four placements: bottom/top × start/end (logical, RTL-safe)
  • Flips to the other edge when the panel would leave the viewport
  • MenuGroup for labeled sections, MenuSeparator for dividers
  • Destructive variant for irreversible actions

Usage

---
import { Menu, MenuItem, MenuSeparator } from "astro-ui";
---

<Menu id="row-actions" triggerLabel="Row actions">
  <MenuItem shortcut="⌘E">Edit</MenuItem>
  <MenuItem shortcut="⌘D">Duplicate</MenuItem>
  <MenuSeparator/>
  <MenuItem variant="destructive" shortcut="⌫">Delete</MenuItem>
</Menu>

Demos

Row actions

<Menu id="dm-menu-row" triggerLabel="Row actions">
  <MenuItem shortcut="⌘E">Edit</MenuItem>
  <MenuItem shortcut="⌘D">Duplicate</MenuItem>
  <MenuSeparator/>
  <MenuItem variant="destructive" shortcut="⌫">Delete</MenuItem>
</Menu>

Grouped actions with icons

<Menu id="dm-menu-groups" triggerLabel="Project actions">
  <MenuGroup label="Project">
    <MenuItem>
      <span slot="icon" aria-hidden="true">✎</span>
      Rename project
    </MenuItem>
    <MenuItem>
      <span slot="icon" aria-hidden="true">⧉</span>
      Duplicate
    </MenuItem>
  </MenuGroup>
  <MenuSeparator/>
  <MenuGroup label="Danger zone">
    <MenuItem variant="destructive" disabled>Transfer ownership</MenuItem>
    <MenuItem variant="destructive">Delete project</MenuItem>
  </MenuGroup>
</Menu>

Panel open on first paint

<Menu id="dm-menu-open" triggerLabel="Filters" open placement="bottom-start">
  <MenuItem>Newest first</MenuItem>
  <MenuItem>Most reactions</MenuItem>
  <MenuSeparator/>
  <MenuItem>Only my drafts</MenuItem>
</Menu>

API

Props

PropTypeDefaultDescription
placement"bottom-start" | "bottom-end" | "top-start" | "top-end""bottom-end"Corner of the trigger the panel aligns to.
openbooleanfalseRender the panel open on first paint.
disabledbooleanfalseDisable the trigger.
bareTriggerbooleanfalseDrop the trigger's own border/background.
triggerLabelstring"Actions"Accessible name for the trigger.
classstring—Merged via cn(); Tailwind classes welcome.
idstring—Element id.
ariaLabelstring—Accessible name.

Slots

SlotDescription
triggerTrigger content; defaults to a ⋯ glyph.
(default)MenuItem, MenuGroup, and MenuSeparator children.

Accessibility

Implemented accessibility features — semantics, ARIA wiring, and keyboard behavior that ship with the component.

  • Trigger is a real button with aria-haspopup="menu" and aria-expanded
  • Panel is role="menu" labelled by its trigger
  • Items are native buttons/links with role="menuitem" and tabindex="-1"
  • Disabled items use aria-disabled and are skipped by keyboard navigation
  • Shortcut hints are aria-hidden — they mirror platform bindings, not app state

FAQ

How does MenuItem behave?

It renders `<a>` when `href` is set and `<button type="button">` otherwise, with `role="menuitem"` and `tabindex="-1"`. Props: `variant` (default/destructive), `href`, `disabled`, `shortcut`, `class`, `id`, plus an `icon` slot.

What about MenuGroup and MenuSeparator?

MenuGroup takes a `label` and renders `role="group"` labelled by that heading, so screen readers name the section. MenuSeparator is an `<hr role="separator">`, skipped by arrow-key navigation.

Do submenus work?

Not yet. Nested flyouts need a second positioning layer plus hover-intent timers; until then, split large menus into separate entry points so every action stays one level deep.

How do I run an action?

Attach a click listener to the MenuItem (or its `id`) in your own script. The menu closes and restores focus before your handler runs, so the action never loses its trigger.

Changelog

Version history mentioning Menu.

v0.1.0 — 2026-11-11
added
  • Overlay (8): Dialog, AlertDialog, Sheet, Tooltip, Popover, HoverCard, Lightbox, Menu.
  • Native-first interactive behavior: Dialog, AlertDialog, and Sheet on <dialog>; Accordion and Collapsible on <details>; Tabs, Menu, Tree, and Combobox with WAI-ARIA roles and scoped keyboard scripts.