Dropdown
Introduction
A list of actions or options that appears when triggered. Supports custom placements with automatic viewport-overflow flipping, an optional arrow with center-pointing geometry, checkbox/radio/group items, cascading submenus, and click / hover / context menu trigger modes.
Usage
Basic Dropdown
import { Dropdown, Button } from "../components/ui";
export default function MyPage() {
return (
<Dropdown
trigger={<Button>Open Dropdown</Button>}
items={[
{ type: "item", label: "Edit", value: "edit" },
{ type: "separator" },
{ type: "checkbox", label: "Bold", value: "bold", checked: true },
{
type: "radio-group",
value: "theme",
label: "Theme",
items: [
{ type: "radio", label: "Light", value: "light" },
{ type: "radio", label: "Dark", value: "dark" },
],
},
]}
/>
);
}
Button with Dropdown Menu
import { Dropdown } from "../components/ui";
export default function Page() {
return (
<Dropdown.Button
type="primary"
items={[{ type: "item", label: "Submit & Close", value: "close" }]}
onClick={() => console.log("Primary click!")}
>
Submit Action
</Dropdown.Button>
);
}
Custom styling & semantic classNames
<Dropdown
trigger={<Button>Styled Dropdown</Button>}
classNames={{
content: "custom-menu-card",
item: "custom-menu-item"
}}
styles={{
content: { boxShadow: "0 4px 20px rgba(0,0,0,0.15)" }
}}
items={[{ type: "item", label: "Custom Styled Item", value: "styled" }]}
/>
Center-pointing Arrow
<Dropdown
trigger={<Button>Center Arrow</Button>}
arrow={{ pointAtCenter: true }}
placement="bottomRight"
items={[{ type: "item", label: "Item 1", value: "1" }]}
/>
Groups and cascading submenus
<Dropdown
trigger={<Button>Open Dropdown</Button>}
items={[
{
type: "group",
label: "File",
items: [
{ type: "item", label: "New", value: "new" },
{ type: "item", label: "Open", value: "open" },
],
},
{ type: "separator" },
{
type: "submenu",
label: "Share",
items: [
{ type: "item", label: "Email", value: "email" },
{ type: "item", label: "Link", value: "link" },
],
},
]}
/>
Custom placement, arrow, and hover trigger
<Dropdown
trigger={<Button>Hover Me</Button>}
placement="bottomRight"
triggerMode="hover"
arrow={true}
mouseEnterDelay={100}
mouseLeaveDelay={150}
items={[
{ type: "item", label: "Profile", value: "profile" },
{ type: "item", label: "Settings", value: "settings" },
{ type: "separator" },
{ type: "item", label: "Logout", value: "logout" },
]}
/>
Context menu
A trigger wired for "contextMenu" opens on right-click, anchored
at the pointer, instead of behaving like a button:
<Dropdown
trigger={<div>Right-click this area</div>}
trigger={"contextMenu"}
items={[
{ type: "item", label: "Copy", value: "copy" },
{ type: "item", label: "Paste", value: "paste" },
]}
/>
Controlled open state
const [open, setOpen] = useState(false);
<Dropdown
open={open}
onOpenChange={setOpen}
trigger={<Button>Open Dropdown</Button>}
items={[{ type: "item", label: "Edit", value: "edit" }]}
/>;
CMS Page Builder
This component is available as a menu block in the Page Builder (content/pages/*.json) — the CMS block is labelled "Menu" but renders through this Dropdown component:
{
"type": "menu",
"triggerText": "Open Dropdown",
"items": [
{ "type": "item", "label": "Edit", "value": "edit" },
{ "type": "separator" },
{ "type": "item", "label": "Delete", "value": "delete" }
]
}
Props
Dropdown
| Prop | Type | Description | Default |
|---|---|---|---|
trigger | JSX.Element | Element that opens the menu when activated. | - |
items | DropdownItem[] | The menu items to render. | - |
open | boolean | Whether the menu is open (controlled). | - |
defaultOpen | boolean | Whether the menu is open by default (uncontrolled). | false |
disabled | boolean | Disables every trigger mode and renders the trigger inert. | false |
interactive | boolean | Forces hydration as an island. Defaults to true. | true |
| arrow | `boolean \ | { pointAtCenter?: boolean }` | Show a pointer arrow pointing from the menu to the trigger. Can align exactly with the trigger's center. | false |
| placement | string | Menu placement: "top" \ | "topLeft" \ | "topRight" \ | "bottom" \ | "bottomLeft" \ | "bottomRight" \ | "left" \ | "leftTop" \ | "leftBottom" \ | "right" \ | "rightTop" \ | "rightBottom". Dash-case aliases are also accepted. | "bottomLeft" |
| trigger | `("click" \ | "hover" \ | "contextMenu" \ | "contextDropdown")[] \ | string` | Trigger interaction modes to open/close the menu. (Aliased as triggerMode). | ["click"] |
| mouseEnterDelay | number | Delay in ms before opening when trigger includes "hover". | 150 |
| mouseLeaveDelay | number | Delay in ms before closing when trigger includes "hover". | 100 |
| closeOnEscape | boolean | Close when Escape is pressed. | true |
| onOpenChange | `(open: boolean, info?: { source: 'trigger' \ | 'menu' }) => void` | Called when the menu opens or closes. source specifies what triggered the action. | - |
| onSelect | (value: string) => void | Called with an item's value when it is activated. | - |
| size | `"xs" \ | "sm" \ | "md" \ | "lg" \ | "xl"` | The size of the menu. | "md" |
| class | string | Custom CSS classes for the root element. | - |
| contentClass | string | Custom CSS classes for the content element. | - |
| positionerClass | string | Custom CSS classes for the positioner element. | - |
| destroyOnHidden | boolean | Whether to destroy/unmount the popup content from DOM when hidden. (Aliased as destroyPopupOnHide). | false |
| popupRender | (menu: JSX.Element) => JSX.Element | Customize/wrap the popup content. (Aliased as dropdownRender). | - |
| classNames | Record<string, string> | Custom CSS classes for each semantic structure component inside the Dropdown. | - |
| styles | Record<string, any> | Custom inline styles for each semantic structure component inside the Dropdown. | - |
classNames and styles slots:
root(orpositioner): The overlay absolute positioning container.content: The popup card containing list items.item: Individual list item (including checkbox and radio items).trigger: The main trigger button/wrapper.arrow: The outer arrow wrapper.arrowTip: The styled inner diamond.
DropdownItem
| Property | Type | Description |
|---|
| type | `"item" \ | "separator" \ | "checkbox" \ | "radio" \ | "radio-group" \ | "submenu" \ | "group"` | The kind of menu entry. |
| label | string | Display text (for item, checkbox, radio, submenu, and optionally group). |
| value | string | Unique value (for item, checkbox, radio, radio-group). |
| checked | boolean | Checked state (for checkbox, radio). |
| icon | JSX.Element | Leading icon (for item, checkbox, radio, submenu). |
| indicator | JSX.Element | Custom trailing indicator element (for item). |
| items | DropdownItem[] | Nested items (for radio-group, submenu, group). Submenu/group items may themselves be any DropdownItem, including further submenus. |
| disabled | boolean | Whether the item (or, for submenu, the whole nested menu) is disabled. |
| class | string | Custom CSS classes for the item. |
Dropdown.Button (DropdownButton)
A Button with a dropdown menu, rendered as a continuous attached button group.
| Prop | Type | Description | Default |
|---|
| type | `"default" \ | "primary" \ | "dashed" \ | "link" \ | "text" \ | "solid" \ | "outline" \ | "subtle" \ | "plain" \ | "surface"` | The type of buttons to render. | "outline" |
| danger | boolean | Renders buttons with danger visual theme. | false |
| disabled | boolean | Disables both the primary button and the dropdown trigger. | false |
| loading | boolean | Renders the primary button in loading/busy state. | false |
| onClick | (e: MouseEvent) => void | Click event handler for the left/primary button. | - |
| icon | JSX.Element | Icon for the right/trigger button. | <EllipsisIcon /> |
| buttonsRender | (buttons: JSX.Element[]) => JSX.Element[] | Custom render function to customize both buttons. | - |
Supports all common Dropdown props such as items, placement, arrow, classNames, styles, etc.
Note
Please ensure that trigger accepts onMouseEnter, onMouseLeave, onFocus,
and onClick — the trigger element is cloned in place with these (and the
relevant ARIA/data-*) attributes attached, rather than wrapped.
Limitations
The interactive island positions the menu relative to its trigger's own
wrapper (position: absolute, not a portal): it flips to the opposite side
when the requested placement would overflow the viewport, and clamps the
cross axis so the menu never renders off-screen. It does not track scroll
containers or resize observers the way Floating UI does — repositioning only
re-runs on window resize while open, and scrolling the page moves the menu
along with the trigger for free. A menu opened from deep inside a scrolling
or overflow: hidden container can still be visually clipped by that
container, the same tradeoff Popover and Tooltip make.
Context menus and submenus are the
exception: since they aren't anchored to a trigger's box (a context menu
opens at the pointer; a submenu opens beside a menu item), they're positioned
with position: fixed and hand-rolled coordinates instead, and are re-run on
Escape/scroll/resize like the rest of the menu.
Each submenu is its own nested interactive island, scoped via a
data-overlay-root marker so a parent menu's positioning and outside-click
logic don't reach into a submenu's own content (and vice versa). Selecting a
regular item closes every open menu level it bubbles through; toggling a
checkbox/radio item inside any level keeps the whole stack open.
The _closed exit animation (slide-fade-out) plays before the menu is
actually removed from layout — closing doesn't hide it instantly.