Skip to content

Navigation

Menu

A compact list of actions opened by a button.

When to use it: Use it for actions, not choosing a persistent form value.

On this page

Example

Loading example…
Menu.tsxtsx
import {
  Menu,
  MenuItem,
  MenuList,
  MenuPopover,
  MenuSection,
  MenuSeparator,
  MenuTrigger,
} from "@comp0/react";
import { ChevronRightIcon } from "@heroicons/react/16/solid";

export function Example() {
  return (
    <Menu>
      <MenuTrigger className="rounded border border-zinc-950/10 px-3 py-2.5 text-base text-zinc-800 select-none sm:py-2 sm:text-sm dark:border-white/10 dark:text-zinc-100">
        Actions
      </MenuTrigger>
      <MenuPopover
        placement="bottom start"
        offset={8}
        className="w-44 rounded border-0 bg-white p-1 shadow-lg ring-1 ring-zinc-950/10 dark:bg-zinc-900 dark:shadow-none dark:ring-white/10"
      >
        <MenuList>
          <MenuSection aria-label="Document" className="grid">
            <MenuItem
              className="w-full rounded px-3 py-2.5 text-left text-base text-zinc-800 select-none hover:bg-zinc-100 sm:py-2 sm:text-sm dark:text-zinc-100 dark:hover:bg-zinc-800"
              value="rename"
            >
              Rename
            </MenuItem>
            <MenuItem
              className="w-full rounded px-3 py-2.5 text-left text-base text-zinc-800 select-none hover:bg-zinc-100 sm:py-2 sm:text-sm dark:text-zinc-100 dark:hover:bg-zinc-800"
              value="duplicate"
            >
              Duplicate
            </MenuItem>
          </MenuSection>
          <MenuSeparator className="my-1 h-px bg-zinc-950/10 dark:bg-white/10" />
          <Menu>
            <MenuTrigger className="flex w-full items-center justify-between rounded px-3 py-2.5 text-left text-base text-zinc-800 select-none hover:bg-zinc-100 sm:py-2 sm:text-sm dark:text-zinc-100 dark:hover:bg-zinc-800">
              Share to <ChevronRightIcon className="size-4 text-zinc-400" aria-hidden="true" />
            </MenuTrigger>
            <MenuPopover
              placement="right top"
              offset={4}
              className="w-40 rounded border-0 bg-white p-1 shadow-lg ring-1 ring-zinc-950/10 dark:bg-zinc-900 dark:shadow-none dark:ring-white/10"
            >
              <MenuList>
                <MenuItem
                  className="w-full rounded px-3 py-2.5 text-left text-base text-zinc-800 select-none hover:bg-zinc-100 sm:py-2 sm:text-sm dark:text-zinc-100 dark:hover:bg-zinc-800"
                  value="email"
                >
                  Email
                </MenuItem>
                <MenuItem
                  className="w-full rounded px-3 py-2.5 text-left text-base text-zinc-800 select-none hover:bg-zinc-100 sm:py-2 sm:text-sm dark:text-zinc-100 dark:hover:bg-zinc-800"
                  value="copy-link"
                >
                  Copy link
                </MenuItem>
              </MenuList>
            </MenuPopover>
          </Menu>
        </MenuList>
      </MenuPopover>
    </Menu>
  );
}

Anatomy

Dashed frames are invisible state providers; shaded shapes own real DOM. Numbered pins match the list below.

A wireframe sketch of the assembled component. Each numbered marker matches a part in the list that follows.

  1. Menu

    Open-state provider; nest a whole Menu inside a MenuPopover to create a submenu. Does not add a DOM element.

  2. MenuTrigger

    Button that opens the menu; inside a parent menu it becomes the submenu item. Owns a DOM element.

  3. MenuPopover

    Role-less floating surface around the menu. Owns a DOM element.

  4. MenuListOptional

    Menu collection and keyboard interaction boundary. Owns a DOM element.

  5. MenuSectionOptional

    Optional labelled section. Owns a DOM element.

  6. MenuSeparatorOptional

    Rule between groups of items. Owns a DOM element.

  7. MenuItem

    Action item. Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Start Menu with MenuTrigger.

  2. 2

    Add the supporting parts

    Place MenuPopover after the trigger, then put MenuItem children inside MenuList.

  3. 3

    Make the behavior clear

    Give MenuList an aria-label when its purpose is not obvious.

    Exampletsx
    <Menu>
      <MenuTrigger>Actions</MenuTrigger>
      <MenuPopover>
        <MenuList aria-label="Actions">
          <MenuItem>Archive</MenuItem>
        </MenuList>
      </MenuPopover>
    </Menu>;

Keyboard

Opens from the trigger to the first item, or moves down.
Opens from the trigger to the last item, or moves up.
Home
Moves to the first item.
End
Moves to the last item.
Opens inline-forward or closes inline-backward; physical keys reverse in RTL. · submenu
Activates the focused item.
Space
Activates the focused item.
Esc
Closes and returns focus to trigger.
Closes the menu and moves on.

Forms and accessibility

No native form behavior.

Accessibility checklist

  • Give MenuList an aria-label when trigger text is vague.
  • Use MenuItem for actions, not a form selection.
  • Submenu open and close arrows mirror in RTL so they continue to follow the panel's visual direction.
  • Return focus to the trigger when the menu closes.

API reference

Importtsx
import { Menu, MenuPopover, MenuList, MenuItem, MenuSection, MenuSeparator, MenuTrigger } from "@comp0/react";

Menu

Context only

Open-state provider; nest a whole Menu inside a MenuPopover to create a submenu.

PropTypeDescription
openbooleanControlled open state.
defaultOpenbooleanInitial open state.
onToggle(open: boolean) => voidReceives the next open state.

MenuTrigger

DOM element

Button that opens the menu; inside a parent menu it becomes the submenu item.

PropTypeDescription
disabledbooleanDisables opening.
asElementType | FragmentFragment merges the trigger onto your own element child.

MenuPopover

DOM element

Role-less floating surface around the menu.

PropTypeDescription
placementPopoverPlacementTrigger side to open on, such as "bottom start" or "right top" for submenus; flips when there is no room.
offsetnumberPixel gap between the trigger and the menu.

MenuList

OptionalDOM element

Menu collection and keyboard interaction boundary.

PropTypeDescription
aria-labelstringNames the menu when the trigger text is vague.
idstringOverrides the collection id targeted by the trigger.

MenuSection

OptionalDOM element

Optional labelled section.

PropTypeDescription
aria-labelstringNames the group of items.

MenuSeparator

OptionalDOM element

Rule between groups of items.

MenuItem

DOM element

Action item.

PropTypeDescription
onClick(event) => voidRuns the action; preventDefault keeps the menu open.
valuestringOptional identity for typeahead and data-value.
disabledbooleanDisables the action.
textValuestringOverrides the text crawled from children when markup makes it ambiguous.

Style hooks

Attributes that appear while a state is true. Target them with Tailwind data variants such as data-open:bg-zinc-100, or with any CSS selector.

MenuTrigger

Style hookMeaning
[data-open]The menu is open.

MenuPopover

Style hookMeaning
[data-open]The menu is open.
:popover-openNative pseudo-class equivalent.

MenuItem

Style hookMeaning
:focus-visibleThe item has visible keyboard focus.
[data-disabled]The item is disabled.

Keep exploring