Skip to content

Menu

A list of actions on a temporary surface. Icons and trailing text, checkbox and radio items, groups and submenus, with arrow-key navigation.

Version 0.4.1 ·

Menu

Installation

bunx --bun shadcn@latest add @dga/menu

Usage

page.tsx
tsx
import { Edit02Icon, Delete02Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { Button } from "@/components/ui/button";
import {
  Menu,
  MenuContent,
  MenuItem,
  MenuSeparator,
  MenuTrigger,
} from "@/components/ui/menu";

export function RequestActions() {
  return (
    <Menu>
      <MenuTrigger render={<Button variant="outline" />}>Actions</MenuTrigger>
      <MenuContent>
        <MenuItem>
          <HugeiconsIcon icon={Edit02Icon} aria-hidden />
          Edit request
        </MenuItem>
        <MenuSeparator />
        <MenuItem>
          <HugeiconsIcon icon={Delete02Icon} aria-hidden />
          Withdraw request
        </MenuItem>
      </MenuContent>
    </Menu>
  );
}

Use a Menu for a set of actions on one thing: a table row, a card, a profile. For choosing a value that is saved in a form, use Select. Items run an action when they are chosen. Put an icon first (it is decorative), the label, then optional trailing content such as MenuShortcut. For a link, render an anchor: <MenuItem render={<a href="/" />}>. Keep the menu to about seven items; split a longer one with MenuSeparator and MenuGroup.

Anatomy

  • Trigger: the button that opens the menu.
  • Menu (role="menu"): background-menu, a 1px border, radius-md, shadow-2xl.
  • Item (role="menuitem"): 40px high, 8px padding, an optional icon and trailing content.
  • Checkbox and radio items: show a check mark or a dot on the end side when chosen.
  • Group and label, separator: structure for longer menus.
  • Submenu: MenuSub with a MenuSubTrigger (chevron) and MenuSubContent.

Examples

Checkbox and radio items

MenuCheckboxItem turns something on or off. MenuRadioGroup with MenuRadioItem chooses one. Groups get a MenuLabel.

Checkbox and radio items

MenuSub opens a second list beside its item, on the end side. It opens with Enter, Space or the arrow that points at it, and closes with the opposite arrow.

Submenu

Stay open and controlled

A checkbox or radio item can set closeOnClick={false} so the menu stays open while the person changes several options. Pass open and onOpenChange to Menu to read or set its state. An ordinary item still closes the menu.

Stay open and controlled

RTL

Menus align to the start edge of the trigger (the right edge in Arabic). A submenu opens on the end side, so on the left, and its chevron is mirrored. Left and Right swap: Left opens a submenu in Arabic. Digits and shortcuts stay left to right.

LTR (English)
RTL (Arabic)

Accessibility

KeyAction
EnterOn the trigger, opens the menu and focuses the first item. On an item, chooses it.
SpaceChooses the focused item (or opens the menu from the trigger).
ArrowDownOpens the menu from the trigger; moves to the next item.
ArrowUpMoves to the previous item.
HomeMoves to the first item (and End to the last).
ArrowRightOpens a submenu (ArrowLeft in Arabic); the opposite arrow closes it.
A to ZMoves to the next item that starts with that letter.
EscapeCloses the menu and returns focus to the trigger.
  • Built on Base UI Menu: role="menu", items with role="menuitem", menuitemcheckbox and menuitemradio, and the trigger has aria-haspopup and aria-expanded.
  • Only one item is in the tab order at a time (roving focus); the arrow keys move between items and focus returns to the trigger on close.
  • Hover and keyboard highlight show a fill; keyboard focus adds a 2px outline inside the item (3:1 or better). A selected item has a tint, a check mark or a dot, so it never relies on colour alone.
  • Text is 4.5:1 and icons 3:1 or better in every state, in light and dark. A disabled item stays in the arrow-key order so it can be found, but cannot be chosen.

API Reference

Menu

A list of actions on a temporary surface (DGA: Menu). Arrow keys move through the items, Home and End jump to the first and last, typing a letter jumps to a match, Escape closes it and focus returns to the trigger. Submenus open on the end side and mirror on Arabic pages. A disabled item can still be reached with the arrow keys, but it cannot be chosen.

Menu props
PropTypeDefault
boolean

Whether the menu is currently open.

No default
boolean

Whether the menu is initially open. To render a controlled menu, use the `open` prop instead.

No default
(open: boolean) => void

Event handler called when the menu is opened or closed.

No default

Also accepts every prop of the Base UI Menu (opens in a new tab).

MenuCheckboxItem

An item that can be turned on and off. It shows a check mark while it is on.

MenuCheckboxItem props
PropTypeDefault
boolean

Whether the checkbox item is currently ticked. To render an uncontrolled checkbox item, use the `defaultChecked` prop instead.

No default
(checked: boolean) => void

Event handler called when the checkbox item is ticked or unticked.

No default

Also accepts every prop of the Base UI Menu (opens in a new tab).

MenuContent

The popup list. It opens below the trigger, aligned to its start side.

Also accepts every prop of the Base UI Menu (opens in a new tab).

MenuGroup

Groups related items under a MenuLabel.

Also accepts every prop of the Base UI Menu (opens in a new tab).

MenuItem

One action. Put an icon first, the label, then an optional MenuShortcut, Tag or Switch. Use `render` to make it a link: `<MenuItem render={<a href="/" />}>`.

MenuItem props
PropTypeDefault
boolean

Whether the component should ignore user interaction.

No default
boolean

Whether to close the menu when the item is clicked.

No default
(event) => void

The click handler for the menu item.

No default

Also accepts every prop of the Base UI Menu (opens in a new tab).

MenuLabel

The heading of a MenuGroup.

Also accepts every prop of the Base UI Menu (opens in a new tab).

MenuRadioGroup

Groups MenuRadioItem options so only one is chosen.

MenuRadioGroup props
PropTypeDefault
(value: string) => void

Function called when the selected value changes.

No default
string

The controlled value of the radio item that should be currently selected. To render an uncontrolled radio group, use the `defaultValue` prop instead.

No default

Also accepts every prop of the Base UI Menu (opens in a new tab).

MenuRadioItem

One option of a MenuRadioGroup. The chosen one shows a dot.

MenuRadioItem props
PropTypeDefault
string

Value of the radio item. This is the value that will be set in the MenuRadioGroup when the item is selected.

No default

Also accepts every prop of the Base UI Menu (opens in a new tab).

MenuSeparator

A line between groups of items.

Also accepts every prop of the Base UI Menu (opens in a new tab).

MenuShortcut

Trailing text of an item, such as a shortcut or a count. Digits and symbols stay left to right.

Also accepts every prop of the Base UI Menu (opens in a new tab).

MenuSub

Groups a MenuSubTrigger and its MenuSubContent.

Also accepts every prop of the Base UI Menu (opens in a new tab).

MenuSubContent

The submenu list. It opens beside its trigger on the end side.

Also accepts every prop of the Base UI Menu (opens in a new tab).

MenuSubTrigger

An item that opens a submenu. Its chevron points to the end side and mirrors in RTL.

Also accepts every prop of the Base UI Menu (opens in a new tab).

MenuTrigger

The button that opens the menu. Render your own Button with `render`.

Also accepts every prop of the Base UI Menu (opens in a new tab).

Deviations from DGA

  • Figma's hover fill is button-background-neutral-hovered; the CSS also sets neutral-100 on the same selector. Figma wins. The 2px focus border is an inset outline for keyboard focus only.
  • Disabled items use text-default-disabled, not opacity: .5 as in the CSS: opacity would fade the focus ring and the check mark too.
  • Submenus, checkbox items and radio items are added; the five trail variants are one slot (any element after the label).
ChangeReason
Menu: states and disabledFigma is the source of truth where the CSS disagrees with itself. Opacity on the whole item also fades the focus ring.
Menu: trailing elementOne slot covers every trail variant and the extra items cover the menus that apps actually need.

View all 2 deviations for this component

Contrast

34 pairs checked · all pass WCAG 2.1 AAShow 17 states
Variant / stateLightDark
item text / default18.09:1 AAA14.42:1 AAA
item icon / default18.09:1 3:114.42:1 3:1
shortcut / default10.17:1 AAA10.95:1 AAA
focus ring / default18.09:1 3:115.89:1 3:1
item text / hovered16.44:1 AAA9.59:1 AAA
item icon / hovered16.44:1 3:19.59:1 3:1
shortcut / hovered9.24:1 AAA7.28:1 AAA
focus ring / hovered16.44:1 3:110.57:1 3:1
item text / pressed14.61:1 AAA6.95:1 AA
item icon / pressed14.61:1 3:16.95:1 3:1
shortcut / pressed8.22:1 AAA5.27:1 AA
focus ring / pressed14.61:1 3:17.66:1 3:1
item text / selected4.53:1 AA8.74:1 AAA
item icon / selected4.53:1 3:18.74:1 3:1
shortcut / selected9.72:1 AAA10.14:1 AAA
focus ring / selected17.29:1 3:114.72:1 3:1
group label14.53:1 AAA14.42:1 AAA

Text needs 4.5:1, icons and focus rings 3:1. Disabled states are exempt. Full contrast report