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

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, or moves to the next item.
Opens from the trigger, or moves to the previous item.
Opens the focused submenu item. · submenu
Closes the submenu and refocuses its item. · submenu
Activates 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.
  • 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
open / defaultOpenbooleanControlled or initial 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.

[data-open]

on MenuTrigger, MenuPopover

The menu is open.
:popover-open

on MenuPopover

Native pseudo-class equivalent.
:focus-visible

on MenuItem

The item has visible keyboard focus.
[data-disabled]

on MenuItem

The item is disabled.

Keep exploring