Overlays

Menu / Dropdown

Replaces v0's Dropdown + Menu + MenuItem trio with one data-driven component. v0 took a whole element as its trigger, so the common case put a button inside a button; v1 removed the choice by generating the button itself, and React's v2 Dropdown takes the element back and never wraps it — so your own Button stays exactly one button.

data-scope="menu"

What you get for free

  • Arrow-key navigation and typeahead
  • aria-haspopup / aria-expanded / aria-activedescendant

Usage

<Dropdown
  menu={{
    items: [
      { key: "edit", label: "Edit", icon: "edit" },
      { type: "divider" },
      { key: "delete", label: "Delete", danger: true },
    ],
    onClick: i => run(i.key),
  }}
>
  <Button>Actions</Button>
</Dropdown>
<Menu trigger="Actions" :items="items" @select="d => run(d.value)" />
<Menu trigger="Actions" {items} onSelect={d => run(d.value)} />
<ck-menu trigger="Actions" [items]="items" (select)="run($event.value)" />

Props

PropTypeDefaultNotes
menu{ items: DropdownMenuEntry[]; onClick?: (i: { key }) => void }React firstReact's Dropdown takes one object, so the menu can grow props without competing with the trigger's. Dividers are `{ type: 'divider' }`.
childrenReactNodeReact firstReact's Dropdown takes the trigger ELEMENT and renders it as given — no generated button, so your own Button stays exactly one button.
itemsMenuEntry[]Not in ReactItems and `{ separator: true }` marks. v2 React: `menu.items`, with `{ type: 'divider' }`.
triggerReactNodeNot in ReactTrigger *content*, not a trigger element. React's Dropdown takes the element as `children`, and its own `trigger` is something else entirely — which gesture opens the menu.
triggerVariantVariant"secondary"Not in ReactStyles the generated trigger as a Button.
onSelect(d: { value }) => voidNot in ReactSelection callback.
trigger"hover" | "focus" | "click" | Array<…>"hover"React firstWhich gesture opens it — the SAME name as the row above and a different thing, which is why that one is flagged. Enter, Space and the arrows open it regardless, because that belongs to the role rather than to this prop, and a tap toggles it.
placementPlacement | PlacementAlias"bottom-start"Menu position.

Prop names are identical in all four frameworks. Only two-way binding differs, and each framework uses its own idiom over the same underlying prop. The marked rows are the exception, and a temporary one, in both directions: React first exists in React while the other three adapters catch up, and Not in React is the prop it replaced, still live everywhere else.

Migrating from react-ui-toolkit

v0v1Notes
Dropdown + Menu + MenuItemone Menu with items
Menu with a generated triggerDropdown wrapping your own triggerReact only. The generated button solved nested buttons by removing the choice; taking the element back and never wrapping it solves the same thing without it.
MenuItem.valueitem.key (React Dropdown)
{ separator: true }{ type: 'divider' } (React Dropdown)
onSelect(d => d.value)menu.onClick(i => i.key) (React Dropdown)
MenuItem.keyMenuItem.value
overlayitems
children as the trigger elementtrigger as content<Dropdown><Button/></Dropdown> produced invalid nested buttons.