A list of actions or options that opens from a button, like a More (…) or Account menu. Use with MenuTrigger and MenuItem.
import { useState } from "react";
import { Button, Menu, MenuItem, MenuTrigger } from "@rdloom/react";
export default function MenuBasicExample() {
const [last, setLast] = useState<string>();
return (
<div className="flex items-center gap-3">
<MenuTrigger>
<Button variant="secondary">Actions</Button>
<Menu onAction={(key) => setLast(String(key))}>
<MenuItem id="edit" shortcut="⌘E">
Edit
</MenuItem>
<MenuItem id="duplicate" shortcut="⌘D">
Duplicate
</MenuItem>
<MenuItem id="archive">Archive</MenuItem>
</Menu>
</MenuTrigger>
<p className="text-sm" aria-live="polite">
{last ? `Chose ${last}` : ""}
</p>
</div>
);
}Installation
npx rdloom add menuCopies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs react-aria-components; add --install to install them.
Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/menu.jsonWorks, but without upgrade tracking.
Usage
import { Button, Menu, MenuItem, MenuTrigger } from "@rdloom/react";
<div className="flex items-center gap-3">
<MenuTrigger>
<Button variant="secondary">Actions</Button>
<Menu onAction={(key) => setLast(String(key))}>
<MenuItem id="edit" shortcut="⌘E">
Edit
</MenuItem>
<MenuItem id="duplicate" shortcut="⌘D">
Duplicate
</MenuItem>
<MenuItem id="archive">Archive</MenuItem>
</Menu>
</MenuTrigger>
<p className="text-sm" aria-live="polite">
{last ? `Chose ${last}` : ""}
</p>
</div>Sections
import { Button, Menu, MenuItem, MenuSection, MenuSeparator, MenuTrigger } from "@rdloom/react";
export default function MenuSectionsExample() {
return (
<MenuTrigger>
<Button variant="secondary">Project</Button>
<Menu>
<MenuSection title="Share">
<MenuItem id="invite">Invite people</MenuItem>
<MenuItem id="copy-link">Copy link</MenuItem>
</MenuSection>
<MenuSeparator />
<MenuSection title="Manage">
<MenuItem id="rename">Rename</MenuItem>
<MenuItem id="move">Move to…</MenuItem>
{/* Destructive: red, and the text says exactly what happens. */}
<MenuItem id="delete" variant="danger">
Delete project
</MenuItem>
</MenuSection>
</Menu>
</MenuTrigger>
);
}Selectable
Showing: name, status
import { useState } from "react";
import type { Selection } from "react-aria-components";
import { Button, Menu, MenuItem, MenuTrigger } from "@rdloom/react";
export default function MenuSelectableExample() {
const [columns, setColumns] = useState<Selection>(new Set(["name", "status"]));
return (
<div className="flex items-center gap-3">
<MenuTrigger>
<Button variant="secondary">Columns</Button>
{/* Items stay checked; the menu stays open while toggling several. */}
<Menu selectionMode="multiple" selectedKeys={columns} onSelectionChange={setColumns}>
<MenuItem id="name">Name</MenuItem>
<MenuItem id="status">Status</MenuItem>
<MenuItem id="owner">Owner</MenuItem>
<MenuItem id="updated">Last updated</MenuItem>
</Menu>
</MenuTrigger>
<p className="text-sm">Showing: {columns === "all" ? "all" : [...columns].join(", ")}</p>
</div>
);
}Icon button
import { Button, Menu, MenuItem, MenuTrigger } from "@rdloom/react";
export default function MenuIconButtonExample() {
return (
<MenuTrigger>
{/* An icon-only button needs a name: screen readers say "More actions, menu button". */}
<Button variant="ghost" aria-label="More actions">
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
<circle cx="3" cy="8" r="1.5" />
<circle cx="8" cy="8" r="1.5" />
<circle cx="13" cy="8" r="1.5" />
</svg>
</Button>
<Menu placement="bottom end">
<MenuItem id="profile">Your profile</MenuItem>
<MenuItem id="settings">Settings</MenuItem>
<MenuItem id="sign-out">Sign out</MenuItem>
</Menu>
</MenuTrigger>
);
}API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
placementWhere the menu opens relative to its button. It flips when there isn't room. | "bottom start" | "bottom end" | "top start" | "top end" | "bottom start" |
selectionModenone for actions; single or multiple for options that stay checked, like view settings. | "none" | "single" | "multiple" | "none" |
onActionCalled with the id of the item chosen. | (key: Key) => void | none |
childrenrequiredMenuItem, MenuSection and MenuSeparator elements. | node | none |
Accessibility
Role menu, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.
Keyboard
- Enter, Space or Down Arrow on the button opens the menu and focuses the first item; Up Arrow focuses the last
- Up and Down Arrow move between items, wrapping at the ends; Home and End jump to the first and last
- Typing a letter moves to the next item starting with it
- Enter or Space chooses the item and closes the menu (in a selectable menu, toggles it)
- Esc closes the menu and returns focus to the button
Screen readers announce
- The button is announced as a menu button, collapsed or expanded
- Opening announces a menu and its first item, with its position (1 of 4)
- Section headings are read when you move into a section
- In selectable menus, items are announced as checked or not checked
- Closing announces the menu button again
What your code must do
- The button has aria-haspopup and aria-expanded, and the menu is labelled by it
- Focus returns to the button when the menu closes
- Destructive items say what they do in their text, not only in red
Guidelines
Use it when
- Several actions on one thing, like Edit, Duplicate, Delete on a row
- Account or overflow (…) menus
- View settings a user toggles, like density or visible columns
Avoid it when
- Picking a value in a form: use Select or Combobox
- Navigation between pages: use links
- One or two actions: show them as buttons
Don't
- Putting form fields inside a menu
- A menu button without a visible label or aria-label
- Destructive actions without a confirmation Dialog
Design tokens
The semantic tokens this component uses. Change them once and every component follows; see Design tokens.
--rd-color-surface-raised--rd-color-surface-subtle--rd-color-border-default--rd-color-text-default--rd-color-text-muted--rd-color-feedback-danger--rd-color-focus-ring--rd-radius-overlay--rd-radius-control