Skip to content

Dropdown Menu

Menu surface with `role="menu"` + `menuitem`, roving-tabindex arrow-key navigation, and focus return.

Overlays & Feedback·Available inReactVueAngular·View as Markdown

Preview

tsx

Switch the framework picker (top-right of the panel) to render the same demo live in React, Vue, or Angular — same class names, ARIA, and visual output across all three.

Installation

Sisyphos UI ships unified packages for React, Vue, and Angular. Pick the one that matches your stack — every framework exports the same component classes, ARIA semantics, and CSS tokens.

$ pnpm add @sisyphos-ui/react

Then import the bundled stylesheet once at app entry: import "@sisyphos-ui/react/styles.css";

Usage

Idiomatic usage in each supported framework
import { DropdownMenu, Button } from "@sisyphos-ui/react";

export const RowActions = () => (
  <DropdownMenu
    items={[
      { label: "Edit", onSelect: () => {} },
      { label: "Duplicate", onSelect: () => {} },
      { type: "separator" },
      { label: "Delete", destructive: true, onSelect: () => {} },
    ]}
  >
    <Button variant="outlined">Actions ▾</Button>
  </DropdownMenu>
);

Placement

Anchor the menu to any corner of the trigger.

tsx

Disabled items

Greyed-out items stay visible for discoverability but skip in keyboard navigation.

tsx

API

PropTypeDefaultDescription
items*DropdownMenuItem[]Actions, separators, and labels rendered inside the menu.
children*ReactElementSingle focusable element used as the trigger.
placement"top" | "bottom" | "left" | "right" | "top-start" | "top-end" | "bottom-start" | "bottom-end""bottom-start"Preferred placement; auto-flips to the opposite side if it doesn't fit the viewport.
openbooleanControlled open state.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => voidFires when the menu opens or closes.
headerReactNodeSlot rendered above the items (for `Notifications · 3` style labels).
footerReactNodeSlot rendered below the items (for `View all` links).
emptyStateReactNodeRendered when `items` is empty.
onScrollEnd() => voidFires when the menu scrolls within `scrollEndThreshold` px of the bottom — use for infinite notification lists.
scrollEndThresholdnumber48Pixel distance from the bottom that triggers `onScrollEnd`.
maxHeightnumber | stringCap the menu height so long item lists become scrollable.

The full API including refs, ARIA attributes, and HTML passthroughs lives in the package README on npm.

Accessibility

Adheres to the Menu Button WAI-ARIA design pattern.

  • The trigger is wired with aria-haspopup="menu", aria-expanded, and aria-controls; the list renders role="menu" with role="menuitem" items.
  • Separators and section labels are exposed as role="separator" / role="presentation", so assistive tech skips them during item navigation.
  • Disabled items carry aria-disabled and are excluded from arrow-key navigation.
  • Roving tabIndex keeps exactly one item focusable; hovering an item moves the active position so pointer and keyboard stay in sync.
  • Escape and item activation close the menu and return focus to the trigger.

Keyboard interactions

KeyAction
ArrowDownthenArrowUpMove between enabled items, wrapping at the edges. On a closed menu, ArrowDown opens it.
HomethenEndJump to the first / last enabled item.
EnterthenSpaceOpen a closed menu, or activate the focused item (`onSelect`).
EscClose the menu and return focus to the trigger.
TabClose the menu.
Need more?View on npm →
Was this page helpful?