DropdownMenu
A dropdown menu that displays a list of actionable items in a popup triggered by a button. Use to present action options as a next step in a process, or to offer contextual actions without cluttering the interface. Like the menus of macOS and iOS, the row under the pointer when it is released is the row that acts, the highlight follows a held mouse or finger, a mouse opens it on press and can drag straight into it, and a finger held on the trigger opens it with the finger down.
Installation
npm install @astryxdesign/core@0.7.1 @astryxdesign/theme-neutral@0.7.1 @stylexjs/stylex@0.19.0
Requires React 19+, base CSS and a Theme provider. See Setup.
Dropdown Menu
A dropdown menu that displays a list of actionable items in a popup triggered by a button. Use to present action options as a next step in a process, or to offer contextual actions without cluttering the interface. Like the menus of macOS and iOS, the row under the pointer when it is released is the row that acts, the highlight follows a held mouse or finger, a mouse opens it on press and can drag straight into it, and a finger held on the trigger opens it with the finger down.
| Prop | Type | Default | Description |
|---|---|---|---|
| button | DropdownMenuButtonProps | { label: 'Menu' } | Props for the trigger button (Button props except onClick). Mutually exclusive with trigger. |
| renderTrigger | (props: DropdownMenuTriggerProps) => ReactNode | Render the control the menu hangs off — an IconButton, a chip, an avatar, a list row — instead of the built-in Button. Spread the given props onto it: they carry the press model (a mouse opens on press-down, a held finger opens with the finger down), the keyboard opens, the toggle click, and aria-haspopup/aria-expanded/aria-controls/id; the menu is then named by that control through aria-labelledby. Mutually exclusive with button. | |
| items * | DropdownMenuOption[] | Array of menu entries. Each entry is one of: an action item {label, onClick?, href?, target?, rel?, icon?, description?, endContent?, isDisabled?, variant?, hasCloseOnSelect?, id?} (href makes the row a real link, so a modified click keeps the browser's meaning; variant "destructive" renders it in the error color; endContent holds trailing content such as a keyboard-shortcut hint; id is the row's stable React key, needed only when the array reorders or filters), a divider {type: "divider"}, or a section {type: "section", title?, id?, items: [...action items]}. | |
| presentation | 'popover' | 'bottom-sheet' | 'adaptive' | 'popover' | Presentation surface for data-driven items. 'popover' stays anchored, 'bottom-sheet' always renders the actions in a modal BottomSheet, and 'adaptive' uses a BottomSheet on compact coarse-pointer layouts while remaining anchored elsewhere. Compound children currently support popover only. |
| isMenuOpen | boolean | Controlled open state for the menu. Mounting with true renders the menu open without moving focus into it; focus moves to the first item only when the menu opens after mount. | |
| onOpenChange | (isOpen: boolean) => void | Callback fired when the open state changes. | |
| menuWidth | number | string | Minimum width for the popover presentation. Length values may grow for content; intrinsic and CSS-wide keywords select the preferred inline size. Every form is capped to the available viewport space. Defaults to matching the trigger width up to that cap. | |
| menuMaxHeight | number | Maximum height in pixels for the popover presentation, for a menu that must fit its rows. Lifts the default 300px cap; the viewport still bounds it. | |
| header | ReactNode | Content above the rows and outside the menu itself — a filter field, a toggle. A menu holds only its rows, so a control that is not a row goes here or in footer, never among the rows. It keeps its own keys and presses. With a header or footer the popover is a modal dialog around the menu: Tab and Shift+Tab move through the header, the menu and the footer and wrap at either end, and Escape closes. The bottom sheet shows it above the actions. | |
| footer | ReactNode | Content below the rows and outside the menu itself — a "Create" button. Tab from the menu moves into it, and Tab past its last control wraps to the start of the dialog (see header). The bottom sheet shows it below the actions. | |
| placement | 'above' | 'below' | 'start' | 'end' | 'below' | Popover placement relative to the trigger. Ignored by the bottom-sheet presentation. Logical: start/end resolve against the menu's own inherited direction (RTL mirrors). |
| alignment | 'start' | 'center' | 'end' | 'start' | Popover alignment along the placement axis. Ignored by the bottom-sheet presentation. Logical: start/end follow the menu's own inherited direction (RTL mirrors). |
| onClick | () => void | Callback fired for accepted trigger activation. The trailing click from the same press that light-dismissed the menu is ignored. | |
| hasChevron | boolean | true | Whether to show a chevron icon on the trigger button. Set to false for icon-only triggers. |
| children | ReactNode | Compound-mode menu content: DropdownMenuItem, DropdownMenuDivider, DropdownMenuSubMenu (a flyout on a laptop, a drilled-in view with a Back row on a phone), and the selectable items. Mutually exclusive with items. |
Usage guidance
- Keep menu items concise and action-oriented so users can scan options quickly.
- Use sections and dividers to group related actions when the menu has many items.
- For a short, flat action set, use presentation="bottom-sheet" when product policy calls for a modal touch surface.
- Use presentation="adaptive" when the same short action set should remain anchored on pointer layouts and become a BottomSheet on compact coarse-pointer layouts.
- For a hierarchy that cannot fit as adjacent flyouts on a compact touch surface, a product may explicitly use a drill-in interaction with a Back action.
- Choose presentation explicitly in product code. A compact, coarse-pointer, hover-free media query is one useful policy, but DropdownMenu does not impose a universal device breakpoint.
- When the content is no longer a short list of immediate actions, reevaluate the interaction and choose a component that matches the actual task; content traits alone do not determine the component.
- Avoid: Use a DropdownMenu for navigation; use a navigation component instead.
- Avoid: Place more than 10–12 items in a single menu without grouping them into sections.
Theming API
{
"targets": [
{
"className": "astryx-dropdown-menu",
"visualProps": [
"presentation"
]
},
{
"className": "astryx-dropdown-menu-item",
"visualProps": [
"size",
"variant"
]
},
{
"className": "astryx-dropdown-menu-radio",
"visualProps": [
"size"
],
"states": [
"checked",
"disabled"
]
},
{
"className": "astryx-dropdown-menu-section-heading"
},
{
"className": "astryx-dropdown-menu-divider"
},
{
"className": "astryx-dropdown-menu-indicator-icon"
}
],
"vars": [
{
"name": "--_dropdown-menu-radius",
"description": "Border radius of the menu popup",
"default": "var(--radius-element)",
"private": true
},
{
"name": "--_dropdown-menu-padding",
"description": "Inner padding of the menu popup",
"default": "var(--spacing-1)",
"private": true
}
],
"derived": [
{
"property": "borderRadius",
"vars": [
"--_dropdown-menu-radius"
]
},
{
"property": "padding",
"vars": [
"--_dropdown-menu-padding"
]
}
]
}
DropdownMenuItem
Anatomy
- Trigger button: Button that opens and closes the selected menu presentation.
- Trigger indicator icon: Optional trailing chevron shown by a labeled trigger when hasChevron is enabled.
- Pointer menu surface: Anchored top-level or nested menu panel used by the pointer presentation.
- Pointer action row: Action, selectable option, or submenu trigger row in an anchored menu.
- Icon-rendered item icon: Optional semantic or component icon rendered through Icon at the start of an action row.
- Caller-rendered item start content: Optional arbitrary React content rendered directly at the start of an action row.
- Checkbox indicator: Decorative shared checkbox indicator for a checkbox action row.
- Radio indicator: Decorative shared radio indicator with an additional menu-owned target.
- Pointer section heading: Heading that labels a group of rows in an anchored menu: a data-driven section or a compound DropdownMenuGroup.
- Pointer divider: Divider between groups in an anchored menu.
- Pointer submenu indicator icon: Trailing chevron that identifies an action row as a nested flyout trigger.
- Touch sheet frame: BottomSheet panel, content area, handle, and optional scrim that host touch actions.
- Touch menu surface: Menu-owned content panel rendered inside the touch sheet frame.
- Touch heading: Current action-sheet title, updated when a nested action view is opened.
- Touch action list: Spacious List that groups actions in the touch presentation.
- Touch action row: ListItem button used for an action or drill-in entry in the touch presentation.
- Touch divider: Divider between action groups in the touch presentation.
Dropdown Menu Checkbox Item
An independently checkable option inside DropdownMenu. The row owns menuitemcheckbox semantics while the shared Indicator supplies decorative checkbox paint.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | ReactNode | Primary label text identifying the item. | |
| description | ReactNode | Secondary description text displayed below the label. | |
| icon | IconType | Icon to display before the label. See npx @astryxdesign/cli docs icons for valid semantic names. | |
| value | boolean | Whether the item is checked. Controlled; pair with onChange. | |
| onChange | (checked: boolean) => void | Callback fired with the next checked state when the item is toggled. | |
| isDisabled | boolean | false | Whether the item is disabled. Disabled items expose aria-disabled, block activation, and are currently skipped by DropdownMenu arrow-key navigation. |
| hasCloseOnSelect | boolean | false | Whether toggling the item closes the menu. Checkbox items default to staying open so several can be toggled in a single session. |
| ref | React.Ref<HTMLElement> | Ref forwarded to the row root, the element carrying role="menuitemcheckbox". Register the row with an element-keyed observer or overlay. | |
| endContent | ReactNode | Content to render after the label and description, such as a keyboard shortcut hint or badge. |
Theming API
{
"targets": [
{
"className": "astryx-dropdown-menu-item",
"visualProps": [
"size"
]
}
]
}
Anatomy
- Checkable row: The menuitemcheckbox root that owns the accessible name, checked state, focus, and activation.
- Label/content: Caller content laid out by Item: the required label plus optional description, icon, and end content.
- Checkbox marker: Decorative shared Indicator chrome that visualizes checked and disabled state without adding a second control.
Dropdown Menu Divider
A horizontal rule separating groups of rows in a compound menu. Renders role="separator", so it is never a stop in the arrow-key order. The data-driven equivalent is the {type: "divider"} entry in items; both modes render this component, so they look and theme identically.
| Prop | Type | Default | Description |
|---|---|---|---|
| xstyle | StyleXStyles | StyleX styles applied after the menu spacing. Must be a stylex.create() value: not an inline style object like style={{}}. |
Theming API
{
"targets": [
{
"className": "astryx-dropdown-menu-divider"
}
]
}
Dropdown Menu Group
A titled group of rows in a compound menu. Renders role="group" named by its heading; the heading is plain text, not a menuitem, so arrow keys and typeahead skip it. The data-driven equivalent is the {type: "section", title, items} entry in items; the heading shares its typography and theme target.
| Prop | Type | Default | Description |
|---|---|---|---|
| title | ReactNode | Heading shown above the rows and used as the group's accessible name. Omit for an unnamed group. | |
| children | ReactNode | The rows of the group: DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuSubMenu. | |
| xstyle | StyleXStyles | StyleX styles applied to the group wrapper. Must be a stylex.create() value: not an inline style object like style={{}}. |
Theming API
{
"targets": [
{
"className": "astryx-dropdown-menu-section-heading"
}
]
}
Dropdown Menu Item
Helper component for custom item rendering with consistent styling. Host attributes and DOM event handlers (aria-, data-, id, drag and drop) reach the row root, the element carrying role="menuitem".
| Prop | Type | Default | Description |
|---|---|---|---|
| icon | IconType | Icon to display before the label. See astryx docs icons for valid semantic names. | |
| label | ReactNode | Primary label text. | |
| description | ReactNode | Secondary description text displayed below the label. | |
| onClick | (event: MouseEvent) => void | Callback when the item is selected. A keyboard activation arrives as a synthesized click carrying the key's modifiers. On a row with href it runs before the browser navigates and is skipped for a modified click (⌘, Ctrl, Shift, Alt, middle button), which is left to the browser. | |
| href | string | Address the row navigates to. The row then renders as a real anchor with role="menuitem" (through LinkProvider), so a modified click or a middle click keeps the browser's meaning — a new tab — instead of running onClick. | |
| target | '_blank' | '_self' | Link target. Only used with href. | |
| rel | string | Link relationship. noopener noreferrer is added for target="_blank". Only used with href. | |
| endContent | ReactNode | Additional content rendered after the label and description. | |
| hasCloseOnSelect | boolean | true | Whether activating the item closes the menu. Set false for an action that reports its result on the item itself. |
| variant | 'default' | 'destructive' | 'default' | Visual variant. 'destructive' renders the label, description, and icon in the error color for dangerous actions (e.g. Delete). |
| xstyle | StyleXStyles | StyleX styles for layout customization (margins, positioning, sizing). Must be a stylex.create() value: not an inline style object like style={{}}. | |
| ref | React.Ref<HTMLElement> | Ref forwarded to the row root, the element carrying role="menuitem". Register the row with an element-keyed observer or overlay. |
Dropdown Menu Radio Group
A single-select group of radio menu items (role="group" of menuitemradio). Owns the selected value and lays its items out with the menu's inter-item gap.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | undefined | The currently selected value in the group. Pass undefined when nothing is selected yet. | |
| onChange | (value: string) => void | Callback fired when the selected value changes. | |
| label | string | Accessible name for the group, applied as aria-label so screen readers announce the radios as a named set, e.g. "Sort by". Required. Pass aria-labelledby (via base props) instead when the name already exists as a visible element. | |
| hasCloseOnSelect | boolean | true | Whether selecting a value closes the menu. Radio items default to closing on selection (a single-choice commit). |
| children | ReactNode | The DropdownMenuRadioItems that make up the group. |
Dropdown Menu Radio Item
A single option in a DropdownMenuRadioGroup (role="menuitemradio"). Must be used inside a DropdownMenuRadioGroup.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | The value this item represents within its group. The group's value matches against this to determine the checked state. | |
| label | ReactNode | Primary label text identifying the option. | |
| description | ReactNode | Secondary description text displayed below the label. | |
| icon | IconType | Icon to display before the label. See npx @astryxdesign/cli docs icons for valid semantic names. | |
| isDisabled | boolean | false | Whether this individual radio item is disabled. Disabled items stay focusable (via aria-disabled) so they remain discoverable by keyboard and assistive technology, but selection is blocked. |
| endContent | ReactNode | Content to render after the label and description, such as a badge or metadata. | |
| ref | React.Ref<HTMLElement> | Ref forwarded to the row root, the element carrying role="menuitemradio". Register the row with an element-keyed observer or overlay. |
Dropdown Menu Submenu
A single menu row that reveals a nested flyout of its own items. The row adopts DropdownMenuItem semantics (label / icon / description / isDisabled); its children become the flyout content. Opens inline-end with viewport auto-flip; Right/Enter/Space opens and focuses the first item, Left/Escape closes and returns focus to the trigger (Right/Left swap in RTL). On a phone (a coarse pointer, decided when the menu opened) the row drills in instead: its rows replace the menu's rows in the same box, led by a Back row named "Back to <parent>", and Back, Escape or ArrowLeft return to the row. Host attributes and DOM event handlers (aria-, data-, id) reach the trigger row, the element carrying role="menuitem" and aria-haspopup="menu"; a caller's handlers run after the row's own, except for the keys that open the flyout (the open arrow, Enter, Space) and a click on a disabled row. For data-driven menus, give a menu item a nested items array instead of using this component directly.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | ReactNode | Primary label text for the trigger row. | |
| icon | IconType | Icon to display before the label. See astryx docs icons for valid semantic names. | |
| description | ReactNode | Secondary description text displayed below the label. | |
| children | ReactNode | The flyout menu items: the same components used at the top level (DropdownMenuItem, nested DropdownMenuSubMenu, selectable items). | |
| isDisabled | boolean | false | A disabled submenu renders its trigger row but never opens the flyout. |
| hasSpinner | boolean | false | Show a spinner in place of the caret, e.g. while a lazy submenu's children are loading. |
| menuWidth | number | string | Minimum flyout width. The flyout may grow for its content, but it is capped to the available viewport space. Defaults to intrinsic sizing (min 160px). | |
| onOpenChange | (isOpen: boolean) => void | Called when the flyout opens or closes. | |
| presentation | 'flyout' | 'drill-in' | 'adaptive' | 'adaptive' | How the sub-menu shows its rows. 'flyout' opens them beside the row; 'drill-in' replaces the menu's rows with them and a Back row, in the same box; 'adaptive' drills in when a finger opened the menu (a coarse pointer, decided when the menu opened) and flies out otherwise. |
| xstyle | StyleXStyles | StyleX styles for the trigger row. Must be a stylex.create() value: not an inline style object like style={{}}. | |
| ref | React.Ref<HTMLElement> | Ref forwarded to the trigger row, the element carrying role="menuitem" and aria-haspopup="menu". |
Examples
- Dropdown Menu Group: A compound-mode menu whose rows are titled in groups. Each DropdownMenuGroup renders a role="group" named by its heading; arrow keys and typeahead skip the heading.
- Dropdown Menu Item: Dropdown menu with custom-rendered items using DropdownMenuItem for icons and descriptions.
- Dropdown Menu: A button that opens a dropdown menu with action items. The menu starts open for preview.
- DropdownMenu — Actions: Action menu with dividers separating safe and destructive operations. Use for row-level actions on items like documents, projects, or records.
- DropdownMenu — Adaptive presentation: Chooses a bottom sheet for compact touch surfaces and an anchored popover otherwise. The media query is product policy, while DropdownMenu owns both presentations.
- DropdownMenuGroup — Selectable rows: Titled groups around a radio group and checkbox rows — the rows the data API cannot express, which is why the heading lives in compound mode.
- DropdownMenuItem — Basic: Dropdown menu items with labels and secondary descriptions. Use DropdownMenuItem to render custom menu entries with consistent styling.
- DropdownMenu — Icon Trigger: Overflow menu triggered by an icon-only button with no chevron or label text. Use for row-level actions in tables, cards, or lists where a text button would take too much space.
- DropdownMenu — Disabled: Menu with selectively disabled items based on permissions. Use when some actions require higher privileges, like admin-only operations.
- DropdownMenu — Sections: Menu items organized into titled sections for easy scanning. Use when you have 6+ actions that fall into distinct categories, like Create vs Manage.
- DropdownMenu — Submenu: Action menu with a nested submenu. Hover or Right arrow opens the flyout; Left arrow / Escape closes it. Use to group related destinations or secondary actions without crowding the top level.
Complete example dependencies
Some examples import optional icons. The per-example install command in the examples list identifies exactly what that example needs. For all examples on this page:
npm install @heroicons/react@2.2.0