Skip to content

Menu

A list of actions or options that opens from a button, like a More (…) or Account menu. Use with MenuTrigger and MenuItem.

Actionv0.1.0experimentalWCAG 2.2 AAView spec

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 menu

Copies 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.json

Works, 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.

PropTypeDefault
placement

Where the menu opens relative to its button. It flips when there isn't room.

"bottom start" | "bottom end" | "top start" | "top end""bottom start"
selectionMode

none for actions; single or multiple for options that stay checked, like view settings.

"none" | "single" | "multiple""none"
onAction

Called with the id of the item chosen.

(key: Key) => voidnone
childrenrequired

MenuItem, MenuSection and MenuSeparator elements.

nodenone

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