Actions
IDropdownMenu
A menu opened from a button. Entries are data, and their shape decides what they render as — the same rule ISelect and INavigationMenu follow.
Nothing chosen yet.
Four shapes, one array, decided in this order:
| The entry | Renders as |
|---|---|
The string '-' | A separator |
Has its own items | A submenu |
Has an onSelect | A menu item |
| Has neither | A group label |
Group labels are the intended use:
Danger items
danger colours a destructive entry. It does not confirm anything — pair it with useConfirm() when the action cannot be undone.
const items = [
{ label: 'Archive', onSelect: () => archive() },
'-',
{
label: 'Delete invoice',
danger: true,
onSelect: async () => {
if (await confirm({ title: 'Delete this invoice?' }))
remove()
},
},
]Placement
side and align position the menu against its trigger, and sideOffset sets the distance. Like the tooltip, side is a preference — the menu flips when there is not enough room.
Icons
An entry's icon takes a Hugeicons icon or any component that renders an SVG.
import { Delete02Icon, PencilEdit02Icon } from '@hugeicons/core-free-icons'
const items = [
{ label: 'Edit', icon: PencilEdit02Icon, onSelect: () => edit() },
{ label: 'Delete', icon: Delete02Icon, danger: true, onSelect: () => remove() },
]A header
The header slot puts a block above the items — an account's name and address, the current workspace, a plan. It is read, not chosen: it takes no stop in the arrow-key order and typeahead ignores it, which a menu entry used as a label cannot say.
An entry with no onSelect renders as a group label, which is the right thing for "Recent files" above a list of them — but wrong for an identity block, which is neither a heading for the items below nor something to act on.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | DropdownMenuEntry[] | [] | The menu contents |
align | 'start' | 'center' | 'end' | 'start' | Alignment against the trigger |
alignOffset | number | — | Offset from the start or end alignment options, in px. |
side | 'top' | 'right' | 'bottom' | 'left' | 'bottom' | Preferred side; flips when it will not fit |
sideOffset | number | 4 | Distance from the trigger, in px |
unstyled | boolean | — | Drop built-in classes |
class | string | — | Merged with the built-in classes |
ui | { content?, header?, item?, label?, separator?, subTrigger?, subContent? } | — | Per-element class overrides |
interface DropdownMenuItemOption {
label: string
icon?: IconLike
disabled?: boolean
danger?: boolean
onSelect?: () => void
/** Makes this entry a submenu. */
items?: DropdownMenuEntry[]
}
type DropdownMenuEntry = DropdownMenuItemOption | '-'Slots
| Slot | When to use it |
|---|---|
trigger | The button that opens the menu. Required |
header | A block above the items — an identity, a workspace, a plan |
| default | Replaces the items entirely |
A dropdown menu is for actions. For choosing a value, use ISelect — it has a model, the right ARIA roles, and typeahead.