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
| Prop | Type | Default | Notes |
|---|---|---|---|
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' }`. |
children | ReactNode | — | React firstReact's Dropdown takes the trigger ELEMENT and renders it as given — no generated button, so your own Button stays exactly one button. |
items | MenuEntry[] | — | Not in ReactItems and `{ separator: true }` marks. v2 React: `menu.items`, with `{ type: 'divider' }`. |
trigger | ReactNode | — | Not 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. |
triggerVariant | Variant | "secondary" | Not in ReactStyles the generated trigger as a Button. |
onSelect | (d: { value }) => void | — | Not 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. |
placement | Placement | 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
| v0 | v1 | Notes |
|---|---|---|
Dropdown + Menu + MenuItem | one Menu with items | |
Menu with a generated trigger | Dropdown wrapping your own trigger | React 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.value | item.key (React Dropdown) | |
{ separator: true } | { type: 'divider' } (React Dropdown) | |
onSelect(d => d.value) | menu.onClick(i => i.key) (React Dropdown) | |
MenuItem.key | MenuItem.value | |
overlay | items | |
children as the trigger element | trigger as content | <Dropdown><Button/></Dropdown> produced invalid nested buttons. |