Skip to main content
Graphite UI

Menu

A list of actions that opens from a trigger. Use it to gather commands that act on one thing; use a Select when the reader is choosing a value, and a Popover when the content is more than a list.

Contract 2.1.0Kit · 3 sets · 38 variantsOpen in Figma

Live preview

Rendered by the component itself from the same generated tokens as the rest of the site.

Installation

Install it with the shadcn CLI, along with everything it uses. New project? Set it up first, as Quick start shows.

npx shadcn@latest add https://www.graphite-ui.com/r/menu.json
import { Menu } from '@/components/ui/menu'
import type { MenuItem } from '@/components/ui/menu'

Anatomy

A trigger you render, then items and separators passed as data. Spread the props the trigger receives onto it, so it gets the click, the arrow keys and the ARIA state. Items are buttons the Menu draws for you, so every one of them has the same padding, hover and focus. The contract also lists sub-menus as an optional slot; they are not built yet, so an item cannot open a nested list.

  1. TriggerRequired
  2. Menu itemsRequiredMinimum 1.
  3. SeparatorsOptional
  4. Sub-menusOptionalNot implemented. An item cannot open a nested menu; the items type has no field for one. Recorded here so the slot does not read as shipped.

Variants

Placement opens the list below or above the trigger, aligned to its start edge; it does not flip on its own. Size sets the row height, from 50 down to 26. The kit’s Complex function is what the items hold: a trailing shortcut, and a leading check that indents every row.

Placement: Bottom
Placement: Top
Size: Large
Size: Medium
Size: Small
Size: Extra small
Function: Complex

States

Item states, the ones the kit draws on its private menu list item. Hover steps the row to elevation-02, the same tone-step move Button makes, and lifts the label to on-surface. Focus is a 2px ring inside the row; with hover it keeps the hover fill. A destructive row is neutral at rest, set apart by its delete icon, and fills with danger on hover.

Enabled
Disabled
Destructive

API reference

Generated from the contract, versioned with it, and checked by drift-check. This table cannot describe props the component does not have.

Menu props
PropTypeDefaultNotes
placement'bottom' | 'top'—Behaviour the kit does not draw; the code keeps it (rule 7's tie-break).
align'start' | 'end'—Which edge of the trigger the menu lines up with. Start by default; end is the kit's Overflow Alignment=End (#286).
flushboolean—Sits the menu on its trigger with no gap, as the kit's Menu buttons draw it. Otherwise it stands 4 off.
size'lg' | 'md' | 'sm' | 'xs'—The kit's Size, rows of 50, 42, 34 and 26 (Carbon's padding around Body/3, two over Carbon's rows, recorded as drawn). Medium by default, as the other form controls are.
items——Each item takes label, onSelect, disabled, destructive, and the kit's Complex parts, shortcut (the trailing Shortcut combo) and selected (the leading check, which makes the item a menuitemcheckbox and indents every row so the labels align). Simple and Complex are what the items hold, not a switch.

Design tokens

Every swatch is live. Change the source color in the header and this table repaints, because it reads the same roles the component does.

  • shadowThe lift, shadow-overlay, the kit's Shadows/Menu, cast by the panel and caret as one shape (or by trigger and panel together on a Tab tip). No edge. (from Popover)
  • elevationItem hover, elevation-02, the kit's layer-hover-01, one rung up the ladder from the panel's elevation-01. The same tone-step move as Button's hover.
  • outlineThe separator, through its subtle step (outline-subtle, the kit's border-subtle-00). No longer the panel's edge, which went with Popover's in 2.0.0.
  • on-surface-variantItem labels and shortcuts at rest, the kit's text-secondary.
  • on-surfaceItem labels on hover, the selected check, and the destructive row's delete icon.
  • primaryThe focus ring, through the family's focus step, 2px inside the row; and disabled labels, through its disabled-content step.
  • dangerThe destructive row's hover fill, the kit's Danger hover.
  • on-dangerThe destructive row's label and icon on its danger fill. The kit binds text-on-color, which resolves to onPrimary; on a danger fill the right role is on-danger.
  • textLabels and shortcuts at Body/3 at every size.
  • spacingRow padding and gaps, the panel's 4 above and below, and the separator's 4 after it.
  • radiusPanel corner, and the corner on each item.
  • motionThe shared overlay entrance, inherited from Popover along with the surface. Declared here too so the drift check can hold this component to it rather than trusting the inheritance.

Usage

The contract's prohibitions, written as the choices you will actually face.

Do

  • Keep a Menu to actions on one object, such as rename, duplicate and delete for one theme.
  • Mark delete and remove as destructive, and let the label say what goes. The icon and the hover fill set it apart; the words still have to say it.
  • Group related items with a separator, and put the destructive ones last.
  • Disable an item that does not apply right now instead of removing it, so the list keeps its shape.

Don’t

  • Style a destructive item like a neutral one. It must never read as a harmless choice.
  • Use a Menu to pick a value that stays chosen. That is a Select.
  • Put fields in the list. Items are labels, a shortcut or a check, and handlers; richer content belongs in a Popover.
  • Rely on hover to reveal items. Everything the Menu can do is in the list when it opens.

Accessibility

What the component does for you, and what it leaves to you.

Keyboard
Enter, Space or Down Arrow on the trigger opens the menu on its first item, and Up Arrow opens it on its last. Down and Up move between items and wrap at the ends; Home and End jump to the first and last. Enter or Space chooses an item. Escape closes the menu, and so does Tab, which then carries on to whatever follows the trigger.
Roles
The list is role="menu" and each item role="menuitem", or menuitemcheckbox with aria-checked when it carries selected. The trigger gets aria-haspopup="menu" and aria-expanded.
Focus
Opening the menu moves focus to an item. Every item is tabindex="-1", so the whole menu is one Tab stop and the arrow keys do the rest. A focused item takes a 2px primary-focus ring inside its edge. When the menu closes, focus goes back to the trigger.
Choosing
Choosing an item runs its onSelect and closes the menu. Disabled items are real disabled buttons, so the arrow keys skip them and they cannot be chosen.
Contrast
Labels are on-surface-variant at rest and on-surface on hover, on elevation-01, lifted by shadow-overlay with no edge. A destructive row on hover is on-danger on danger.
Motion
The list fades in on the fast motion step and appears at once under prefers-reduced-motion.

Figma parity

The kit’s Menu page has one public set, Menu, built from a private menu list item that carries the item states shown above. Size is a prop; Function is what the items hold.

Kit axes and how they map to code
Kit axisValuesCodeHow it maps
FunctionSimple · ComplexitemsNo switch. How much the list holds comes from the items you pass, separators included.
SizeLarge · Medium · Small · Extra smallsizelg, md, sm and xs: rows of 50, 42, 34 and 26. Two over Carbon’s, because the kit kept Carbon’s padding around Body/3; built as drawn.
DeleteTrue · FalsedestructiveThe trailing delete icon at rest and the danger fill on hover. The kit’s label there is text-on-color (onPrimary); the code uses on-danger, the right role on a danger fill.
Item: Shortcuts or TriggerShortcut combo · CaretshortcutThe shortcut is text in the trailing slot; the kit draws a modifier glyph and a letter. Caret is a sub-menu trigger, which is not built.
Item: Selected · IndentedTrue · FalseselectedA leading check. Setting selected on any item indents every row, so the labels align.
Item: DividerTrue · False{ kind: separator }A 1px outline-subtle line on the next row’s top edge, then 4.
Item stateEnabled → Danger hover + Focus (private set)—Pseudo-classes in code, plus disabled and destructive on each item. Governance rule 7: a State=Hover variant is not an instruction to add a hover prop. The private set names one state Focus twice; the one with the hover fill is Focus + Hover.