Dropdown Menu
Displays a menu to the user triggered by a button.
DropdownMenu is a classic dropdown menu opened on click (kebab menu, user actions, table row actions). It is based on the Popover pattern, with menu items.
Composition: trigger + content + items. Items can be <a href> or <div> elements with role="menuitem" and data-ui="close": activating one, with a click or with Enter, closes the menu. The ui.js island provides the keyboard: arrow keys, Home and End, type-ahead, Escape and Tab.
Installation
$ bext ui add dropdown-menu
Copy and paste the following into src/components/ui/dropdown-menu.tsx.
This component is interactive. Make sure the shared island is loaded (see Islands). It reacts to data-ui="dropdown-menu" hooks.
Usage
Anatomy
<DropdownMenu>
├── <DropdownMenuTrigger for="menu-id">
│ └── <Button>Open</Button>
└── <DropdownMenuContent id="menu-id" align="start|end">
├── <DropdownMenuLabel>My Account</DropdownMenuLabel>
├── <DropdownMenuSeparator />
├── <DropdownMenuItem href="/profile">Profile</DropdownMenuItem>
└── <DropdownMenuItem>Log out</DropdownMenuItem>API Reference
DropdownMenu
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | — | Wrapper relative inline-block. |
DropdownMenuTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
| for | string | — | Matches the id of the Content. |
DropdownMenuContent
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | — | Matches Trigger.for. |
| align | "start" | "end" | "start" | Horizontal alignment of the panel. |
| ariaLabel | string | — | Accessible name (aria-label) of the menu. Without it the island names the menu after its trigger button (aria-labelledby). |
| className | string | — | min-width:8rem by default, absolute top-full mt-2 border bg-popover p-1 shadow-md. |
DropdownMenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
| href | string | — | If set, renders <a> and navigates. Otherwise a <div> that only closes the menu. |
| disabled | boolean | — | Sets data-disabled and aria-disabled="true": no pointer events, 50% opacity, skipped by the arrow keys. |
| children | ReactNode | — | Label + optional icons. |
DropdownMenuLabel
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | Non-clickable header (section name, e.g. "My Account"). Not focusable. |
DropdownMenuSeparator
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | — | Extra classes on the thin bg-muted line (role="separator") between groups of items. |
Data Attributes
Attributes you can target in custom CSS to override the style for each state.
| Attribute | On | Values | Description |
|---|---|---|---|
| data-ui="toggle-pop" | Trigger | data-target="#id" | Clicking opens or closes. |
| data-overlay data-light | Content | hidden | Light popover: an outside click closes it. |
| data-ui="close" | Item | — | Closes on click and returns focus to the trigger. |
| data-state | Trigger | "open" | "closed" | Set by the island, together with aria-haspopup="menu", aria-expanded and aria-controls on the button inside. |
| data-disabled | Item | present when disabled | Removes pointer events, dims the item and takes it out of keyboard navigation. |
Keyboard
| Key | Action |
|---|---|
| Enter / Space | On the trigger button: opens the menu and focuses the first item. On an item: activates it, closes the menu and returns focus to the trigger. |
| ArrowDown / ArrowUp | On the trigger: opens the menu on the first or the last item. In the menu: moves to the next or previous item, wrapping at the ends. Disabled items are skipped. |
| Home / End | Moves to the first or the last item. |
| A letter | Moves to the next item whose label starts with that letter. |
| Escape | Closes the menu and returns focus to the trigger. |
| Tab | Closes the menu and moves on to the next element after the trigger. |
Accessibility
DropdownMenuTrigger is a <span> around your button. The island sets aria-haspopup="menu", aria-controls and aria-expanded on that button and keeps aria-expanded in sync on every open and close, including a close by outside click or Escape. Put a real <button> inside the trigger.
DropdownMenuContent has role="menu" and is named by its trigger button (aria-labelledby) or by the ariaLabel prop. Every DropdownMenuItem has role="menuitem" and tabindex="-1": items are reached with the arrow keys, not with Tab. DropdownMenuSeparator sets role="separator". DropdownMenuLabel is plain text and is never focused.
Focus: opened from the keyboard, the menu focuses its first item. Opened with the pointer, the menu itself takes focus, so the arrow keys work without highlighting an item the user did not point at; hovering an item focuses it. Closing with Escape or by activating an item returns focus to the trigger.
Examples
Aligned end (kebab menu)
Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.
On This Page