Dropdown Menu
Menu surface with `role="menu"` + `menuitem`, roving-tabindex arrow-key navigation, and focus return.
Preview
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/reactThen import the bundled stylesheet once at app entry: import "@sisyphos-ui/react/styles.css";
Usage
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.
Disabled items
Greyed-out items stay visible for discoverability but skip in keyboard navigation.
API
| Prop | Type | Default | Description |
|---|---|---|---|
| items* | DropdownMenuItem[] | — | Actions, separators, and labels rendered inside the menu. |
| children* | ReactElement | — | Single 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. |
| open | boolean | — | Controlled open state. |
| defaultOpen | boolean | false | Initial open state when uncontrolled. |
| onOpenChange | (open: boolean) => void | — | Fires when the menu opens or closes. |
| header | ReactNode | — | Slot rendered above the items (for `Notifications · 3` style labels). |
| footer | ReactNode | — | Slot rendered below the items (for `View all` links). |
| emptyState | ReactNode | — | Rendered when `items` is empty. |
| onScrollEnd | () => void | — | Fires when the menu scrolls within `scrollEndThreshold` px of the bottom — use for infinite notification lists. |
| scrollEndThreshold | number | 48 | Pixel distance from the bottom that triggers `onScrollEnd`. |
| maxHeight | number | string | — | Cap 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, andaria-controls; the list rendersrole="menu"withrole="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-disabledand are excluded from arrow-key navigation. - Roving
tabIndexkeeps 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
| Key | Action |
|---|---|
| ArrowDownthenArrowUp | Move between enabled items, wrapping at the edges. On a closed menu, ArrowDown opens it. |
| HomethenEnd | Jump to the first / last enabled item. |
| EnterthenSpace | Open a closed menu, or activate the focused item (`onSelect`). |
| Esc | Close the menu and return focus to the trigger. |
| Tab | Close the menu. |