Skip to main content
Dropdown combines a positioned popup, visual rows, keyboard navigation, focus, and dismissal. Your application owns data, filtering, fetching and pagination. SelectionGroup owns independent selection values inside the popup. Import it from twenty-ui/components. Dropdown.Content accepts numeric offsets or functions for sideOffset and alignOffset. Offset functions receive the anchor and popup dimensions, side, and alignment. Use them to preserve an anchored field picker’s overlap when the cell is scaled.

Usage

Choose an interaction type

Set type on Dropdown.Root to match the popup’s contents. Pickers allow creation actions and independent selection groups in the same page, in any order. Each picker group is a listbox, and its options expose aria-selected. Menu groups use checked radio or checkbox menu items. Panel groups use buttons with aria-pressed. Commands retain their mode’s action semantics. Give each group and Search an accessible name. This custom picker is not a native Combobox. Content takes its accessible name from Trigger, or from SubmenuTrigger in a submenu. Pass aria-label on Content when the trigger shows a value, such as the selected option, or when the root has no trigger. An explicit aria-label or aria-labelledby, and a Title inside a Header, take precedence over the trigger name. Select and Popover remain available for direct composition.

Anatomy

Content includes the portal and positioner. Use width, side, align, sideOffset, and alignOffset to control placement, and collisionPadding to keep the popup further from the edges of the viewport before it shifts or flips (5 pixels by default). Section groups rows with shared spacing, and Separator divides sections. A scrollable Section caps its height and scrolls; nest labelled sections inside it to scroll them as one list. Rows use ListItem for their appearance.
Pass custom trigger elements through render so the trigger remains one interactive element. Custom components must forward the supplied props and ref. ActionItem also accepts render for links.

Item actions

ActionItem and OptionItem accept actions for trailing controls, such as a tab’s edit button. Use them in picker and panel pages only: a menu may only contain menu items, so it cannot hold other controls. Set actionsVisibility="always" to keep them visible; the default is "hover". Secondary controls render beside the primary row control and activate independently: they do not close the popup, so close it from the action’s handler when needed. The render prop and ref still apply to the primary control, so it can also serve as a drag handle. Use tabIndex={-1} for a pointer-only secondary action that has a separate keyboard alternative. Keyboard navigation continues to focus and activate the primary control.

Headers

Header lays out a row at the top of the popup. Put a Title in it to name the popup after the title instead of the trigger. Close closes the popup the same way an outside press does. It renders an icon button with an X by default, so give it an aria-label, or pass render and children for another control. Other controls, such as a button that returns to a previous step, can sit in the same row. Use Dropdown.Back when navigating the root’s page history.

Search and selection

Search provides the input and navigation into the result rows. Pass value and onValueChange; compute or fetch matching results in your application. Render Loading while a request is pending and Empty when no results match. Both accept caller-provided status text. Wrap selectable options in SelectionGroup and give each option a stable value unique within the group. An uncontrolled group also needs an id unique within its dropdown tree, including submenus. Independently nested Root components keep their own selection store. Single selection uses a string or null for no selection. Multiple selection uses an array, including [] for no selection.
Pass value and onValueChange for controlled selection. Omitting value, or passing undefined, leaves selection uncontrolled; defaultValue sets its initial value. An uncontrolled group defaults to null, or [] with multiple. Its stable id preserves selection across close and reopen and across page navigation. Removing or filtering an option does not clear its selected identifier or emit a change; the option is selected again when it returns. Use a controlled value when the application needs to clear unavailable choices. Each group changes independently. An option commits to its nearest group, while the active page supplies one ordered collection for arrow navigation across groups and action rows. Repeated activation of a selected single option preserves its value without another change callback. Multiple activation toggles the option in the array. Single selection closes by default; multiple selection stays open. SelectionGroup.closeOnSelect changes the group default, and OptionItem.closeOnSelect overrides it for one option. Action rows use closeOnClick independently. A cancelled change preserves the previous selection and does not automatically close the popup. onValueChange receives the next value and event details with event, reason, trigger, cancel() and isCanceled. The reason is item-press, and event is the original activation event. OptionItem.onSelect receives the same details before its owner commits. Call cancel() in either callback to prevent selection and automatic closing.
Search receives focus when the picker opens. Without search, the first enabled selected option receives focus, or the first enabled row when no selected option is present. While Search has focus and a nonempty query, the first eligible OptionItem across selection groups is highlighted and Enter activates it. Action rows are never picked this way. ArrowDown moves focus to the first enabled row, including actions. Leaving search clears the preview and aria-activedescendant, even when the popup stays open. Returning to search restores the preview for the current query. Enter does nothing for an empty or whitespace-only query, or when no eligible option remains. Composing text with an input method does not activate an option or move focus with arrow keys. When rows are pinned above matches, such as the current value or a “none” row, render them only when they match the search text. The popup stays open when no option matches. Filtering remains caller-owned. Standalone OptionItem still accepts selected and onSelect for callers that own selection themselves. Grouped options derive selection from the group value; do not duplicate that state on the option. Custom selection groups do not provide native form submission, reset, required validation, read-only behavior or Field integration. Use Select when those native form contracts are needed.

Icon grids

Set columns on Section to lay out items in equal-width grid columns. The same count drives keyboard navigation: Left and Right move within a row, while Up and Down move between rows. Navigation skips disabled options and follows the reading direction. When no enabled option is left in the column, Up and Down move to the nearest enabled option of the next row. Up from the first row returns to the search field when no earlier section exists. A short final row uses its last item when the requested column is missing. Inside a submenu, the backward arrow closes the submenu when no enabled option remains to its left. Render each icon as an OptionItem with an accessible label and an icon button through render. Use indicator="none" to show selection through the button background. The render callback receives { selected, multiple, disabled } state; use it to style a selected cell and forward the supplied props and ref.

Keyboard focus

In menu and picker pages, the rows form a single Tab stop. Arrow keys move focus across sections, actions and options, and Home and End move to the first and last enabled rows unless the search field has focus. Moving focus never selects an option; Enter or Space activates the focused row. Tabbing back to the rows returns to the last focused row, or to the first enabled row once that row is gone. Search, Back beside a search, header buttons and other controls inside the popup keep their own Tab stops. Tab and Shift+Tab close a menu, along with its parent menus when focus is in a submenu. In a picker, Tab moves through the popup’s controls and closes the popup when focus leaves it. A panel keeps the native Tab order of its controls.

Pages and submenus

Use Page for navigation that replaces the contents of the same popup. Wrap the first page in Page id="root", or choose another initial page with defaultPage. Set page on an action to navigate without closing. Back returns to the previous page and restores focus to its invoking row. Closing the dropdown resets its page history. A page can override the root’s type. For example, an action menu can open a searchable picker page or a form panel. Search receives focus when entering a searchable page; other pages focus their first row after Back.
To navigate from code, call useDropdownPage in a component rendered inside the root. It returns the current page, goToPage(page), goBack() and canGoBack. For example, an option can return to the previous page after a selection while the popup stays open:
Use Submenu with SubmenuTrigger and a nested Content for a side-by-side menu. Arrow Right enters the submenu; Arrow Left returns to its parent. Selecting a closing action closes the whole dropdown.

Independent nested pickers

Place another Dropdown.Root inside Content when a control opens its own picker, such as a sort direction selector. Selecting an inner option closes only that picker and restores focus to its trigger. Escape and outside presses dismiss one layer at a time. Use Submenu for commands that should close the entire menu tree when selected.

Panels and controlled state

Use type="panel" for form inputs. Input interaction keeps the panel open; a save action can close it. Own open and onOpenChange when another component or a shortcut needs to open the dropdown. Keep that state with the feature that coordinates the interaction. Escape dismisses the popup and restores focus to its trigger. Outside interaction also dismisses it, and the click that dismisses it doesn’t activate what it lands on. Keys pressed inside the popup stay inside it, except Ctrl or Cmd shortcuts the dropdown doesn’t handle, so page-level shortcuts keep working. Content accepts focus and portal options from Popover, including an explicit return-focus target for editor workflows.

Dismissal

Escape, a press outside the popup and Tab out of it close the dropdown. Pass onEscapeKeyDown or onInteractOutside to Root to react before it closes, and call event.preventDefault() to keep it open. event.target is the element pressed outside, or null when focus leaves with Tab, and event.type is the type of the DOM event that triggered it, such as touchmove for a swipe outside the popup. A prevented outside press still reaches the element it lands on.

Anchoring without a trigger

A root can open without a Trigger, for example as a context menu. Control it with open and onOpenChange, and pass anchor to Content: an element, a ref, or a virtual element that returns a rectangle in viewport coordinates. Give Content an aria-label, since there is no trigger to name it after. On close, focus returns to the element that was focused when the popup opened.
Right-clicking elsewhere while the menu is open moves it to the new point.

Labels

Each part owns its text: supply Search.placeholder and native accessible names, Close accessible attributes, and children for Back, Loading, and Empty. Back defaults to Back; loading and empty states have no built-in message. Item parts accept shortcutJoinLabel to replace the default then between sequential shortcut steps. Native aria-labelledby takes precedence over aria-label.

Props

boolean
Whether the popup is initially open when uncontrolled.
string
ID of the Page shown when the popup opens. Closing the popup resets the page history to it. Defaults to root.
boolean
Default for standalone options. Use SelectionGroup.multiple for independently owned selection values.
((event: DropdownDismissEvent) => void)
Called when Escape is about to close the popup. Call event.preventDefault() to keep it open.
((event: DropdownDismissEvent) => void)
Called when a press outside the popup, or Tab out of it, is about to close it. event.target is the element pressed outside, or null after Tab, and event.type is the type of the DOM event that triggered it. Call event.preventDefault() to keep it open.
((open: boolean) => void)
Called with the next open state when the popup opens or closes.
boolean
Whether the popup is open. Use with onOpenChange to control it.
"menu" | "panel" | "picker"
required
Interaction model: menu for commands and links, picker for search and selectable options, or panel for forms. It sets the ARIA roles, initial focus, and keyboard navigation.
string | ((state: PopoverTriggerState) => string | undefined)
CSS class applied to the element, or a function that returns a class based on the component’s state.
number
default:"0"
How long to wait before closing the popover that was opened on hover. Specified in milliseconds.Requires the openOnHover prop.
number
default:"300"
How long to wait before the popover may be opened on hover. Specified in milliseconds.Requires the openOnHover prop.
string
ID of the trigger. In addition to being forwarded to the rendered element, it is also used to specify the active trigger for the popover in controlled mode (with the PopoverRoot triggerId prop).
boolean
default:"true"
Whether the component renders a native <button> element when replacing it via the render prop. Set to false if the rendered element is not a button (e.g. <div>).
boolean
default:"false"
Whether the popover should also open when the trigger is hovered.
ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, PopoverTriggerState>
Allows you to replace the component’s HTML element with a different tag, or compose it with another component.Accepts a ReactElement or a function that returns the element to render.
CSSProperties | ((state: PopoverTriggerState) => CSSProperties | undefined)
Style applied to the element, or a function that returns a style object based on the component’s state.
"center" | "end" | "start"
default:"start"
Alignment along the anchor. Defaults to start.
number | OffsetFunction
default:"0"
Offset along the alignment axis. Accepts a number or positioning function.
Element | VirtualElement | RefObject<Element | null> | (() => Element | VirtualElement | null) | null
An element to position the popup against. By default, the popup will be positioned against the trigger.
string | ((state: PopoverPopupState) => string | undefined)
CSS class applied to the element, or a function that returns a class based on the component’s state.
Padding
default:"5"
Additional space to maintain from the edge of the collision boundary.
HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null
Portal destination, using the scoped theme when omitted. Explicit null waits for a target, matching Popover.Portal.
boolean | RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)
Determines the element to focus when the popover is closed.
  • false: Do not move focus.
  • true: Move focus based on the default behavior (trigger or previously focused element).
  • RefObject: Move focus to the ref element.
  • function: Called with the interaction type (mouse, touch, pen, or keyboard). Return an element to focus, true to use the default behavior, null to fall back to the default behavior, or false/undefined to do nothing.
boolean | RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | void | HTMLElement | null)
Determines the element to focus when the popover is opened. By default, focus moves to the first tabbable element inside the popup, except when the popover is opened by touch — then the popup itself is focused to avoid opening the virtual keyboard.
  • false: Do not move focus.
  • true: Move focus based on the default behavior (first tabbable element or popup).
  • RefObject: Move focus to the ref element.
  • function: Called with the interaction type (mouse, touch, pen, or keyboard). Return an element to focus, true to use the default behavior, null to fall back to the default behavior, or false/undefined to do nothing.
boolean
default:"false"
Keeps the popup mounted while closed.
ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, PopoverPopupState>
Allows you to replace the component’s HTML element with a different tag, or compose it with another component.Accepts a ReactElement or a function that returns the element to render.
"bottom" | "inline-end" | "inline-start" | "left" | "right" | "top"
default:"bottom"
Preferred side of the anchor. Defaults to bottom, or inline-end in a submenu.
number | OffsetFunction
default:"0"
Distance from the anchor. Defaults to 0. Accepts a number or positioning function.
CSSProperties | ((state: PopoverPopupState) => CSSProperties | undefined)
Style applied to the element, or a function that returns a style object based on the component’s state.
Width<string | number>
default:"200"
CSS width of the popup. Numbers are in pixels.
ReactNode
Trailing controls rendered beside the primary row control. Only supported in picker and panel pages, since a menu may only contain menu items. Secondary actions activate independently of the row.
"always" | "hover"
Whether trailing actions appear on hover or remain visible. Defaults to hover.
string
CSS class applied to the row.
boolean
default:"true"
Closes the dropdown after activation, along with parent menus when inside a submenu. Ignored when page is set.
"danger" | "neutral"
Color of the text and icons. danger marks a destructive action.
ReactNode
Supporting text, placed according to descriptionPlacement.
"end" | "inline"
Where the description renders: inline after the content or at the end of the row.
ReactNode
Icon rendered after the content.
boolean
default:"false"
Whether the button should be focusable when disabled.
boolean
Shows a chevron indicating that the item opens a submenu.
boolean
default:"true when render is omitted; false otherwise"
Whether the component renders a native <button> element when replacing it via the render prop. Set to false if the rendered element is not a button (for example, <div>).
string
ID of the Page to show on activation. The popup stays open, and the row shows a chevron by default.
ReactElement<unknown, string | JSXElementConstructor<any>>
Element rendered as the row instead of a native button. Custom components must forward the supplied props and ref.
ShortcutDefinition
Keyboard shortcut keys displayed at the end of the row. Registering the shortcut is up to the application.
string
Text between sequential shortcut steps. Defaults to then.
ReactNode
Icon rendered before the content.
CSSProperties
Inline styles applied to the row.
ReactNode
Trailing controls rendered beside the primary row control. Only supported in picker and panel pages, since a menu may only contain menu items. Secondary actions activate independently of the row.
"always" | "hover"
Whether trailing actions appear on hover or remain visible. Defaults to hover.
string
CSS class applied to the row.
boolean
Overrides the nearest group close policy for this option. A cancelled selection never closes automatically.
"danger" | "neutral"
Color of the text and icons. danger marks a destructive action.
ReactNode
Supporting text, placed according to descriptionPlacement.
"end" | "inline"
Where the description renders: inline after the content or at the end of the row.
ReactNode
Icon rendered after the content.
boolean
default:"false"
Whether the button should be focusable when disabled.
boolean
Shows a chevron indicating that the item opens a submenu.
"check" | "checkbox" | "none"
Selection indicator: a check icon after the content, a checkbox before it, or none. Defaults to checkbox when multiple is set, otherwise check, and to none when selected is omitted.
boolean
default:"true when render is omitted; false otherwise"
Whether the component renders a native <button> element when replacing it via the render prop. Set to false if the rendered element is not a button (for example, <div>).
((eventDetails: DropdownSelectionGroupChangeEventDetails) => void)
Called before the nearest group commits selection, with event, reason, trigger, cancel and isCanceled. Calling cancel() prevents the value change and automatic close.
ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DropdownOptionItemState>
Element or function rendered as the row. Functions receive the supplied props and { selected, multiple, disabled } state. Forward the props and ref to the rendered control.
boolean
Selected state for standalone options. Options inside a SelectionGroup derive it from their owner. Grouped picker options expose aria-selected, menu options expose aria-checked, and panel options expose aria-pressed.
ShortcutDefinition
Keyboard shortcut keys displayed at the end of the row. Registering the shortcut is up to the application.
string
Text between sequential shortcut steps. Defaults to then.
ReactNode
Icon rendered before the content.
CSSProperties
Inline styles applied to the row.
string
Stable option identifier within its nearest SelectionGroup. The group supplies selected state and commits this value on activation.
boolean
Default close policy for owned options: true for single selection, false for multiple selection. Individual options can override it.
string | string[] | null
Initial uncontrolled selection. Defaults to null for single selection and an empty array for multiple selection.
string
Stable identifier unique within the dropdown root. Required for uncontrolled groups, whose values persist under it across popup close and page navigation. Optional for controlled groups.
boolean
Uses an array of selected identifiers when true. Otherwise the value is one identifier or null.
((value: string | null, eventDetails: DropdownSelectionGroupChangeEventDetails) => void) | ((value: string[], eventDetails: DropdownSelectionGroupChangeEventDetails) => void)
Called with the next value and cancellable event details: event, reason, trigger, cancel and isCanceled. Calling cancel() preserves the value and keeps the popup open. Repeated single selection does not emit another change.
ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DropdownSelectionGroupState>
Element or function rendered as the group. Functions receive supplied props and { value, multiple } state. Forward the props and ref.
string | string[] | null
Controlled selection: a string or null for single selection, or an array for multiple selection. Undefined leaves selection uncontrolled.
string | ((state: InputState) => string | undefined)
CSS class applied to the element, or a function that returns a class based on the component’s state.
string | number | readonly string[]
The default value of the input. Use when uncontrolled.
((value: string) => void)
Called with the search text on every change. The application filters the results.
ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, InputState>
Allows you to replace the component’s HTML element with a different tag, or compose it with another component.Accepts a ReactElement or a function that returns the element to render.
CSSProperties | ((state: InputState) => CSSProperties | undefined)
Style applied to the element, or a function that returns a style object based on the component’s state.
string | number | readonly string[]
The value of the input. Use when controlled.
ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>
Allows you to replace the component’s HTML element with a different tag, or compose it with another component.Accepts a ReactElement or a function that returns the element to render.
string | ((state: PopoverTitleState) => string | undefined)
CSS class applied to the element, or a function that returns a class based on the component’s state.
ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, PopoverTitleState>
Allows you to replace the component’s HTML element with a different tag, or compose it with another component.Accepts a ReactElement or a function that returns the element to render.
CSSProperties | ((state: PopoverTitleState) => CSSProperties | undefined)
Style applied to the element, or a function that returns a style object based on the component’s state.
string
required
Accessible name of the close control. Required because the default control only shows an icon.
string | ((state: PopoverCloseState) => string | undefined)
CSS class applied to the element, or a function that returns a class based on the component’s state.
boolean
default:"true"
Whether the component renders a native <button> element when replacing it via the render prop. Set to false if the rendered element is not a button (for example, <div>).
ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, PopoverCloseState>
Allows you to replace the component’s HTML element with a different tag, or compose it with another component.Accepts a ReactElement or a function that returns the element to render.
CSSProperties | ((state: PopoverCloseState) => CSSProperties | undefined)
Style applied to the element, or a function that returns a style object based on the component’s state.
string
required
Identifier matched by defaultPage, the page prop of actions, and goToPage. The page renders only while it is current.
"menu" | "panel" | "picker"
Interaction model while the page is shown. Defaults to the root type.
string
CSS class applied to the row.
"danger" | "neutral"
Color of the text and icons. danger marks a destructive action.
ReactNode
Supporting text, placed according to descriptionPlacement.
"end" | "inline"
Where the description renders: inline after the content or at the end of the row.
ReactNode
Icon rendered after the content.
boolean
default:"false"
Whether the button should be focusable when disabled.
boolean
Shows a chevron indicating that the item opens a submenu.
boolean
default:"true when render is omitted; false otherwise"
Whether the component renders a native <button> element when replacing it via the render prop. Set to false if the rendered element is not a button (for example, <div>).
ReactElement<unknown, string | JSXElementConstructor<any>>
Element rendered as the row instead of a native button. Custom components must forward the supplied props and ref.
ShortcutDefinition
Keyboard shortcut keys displayed at the end of the row. Registering the shortcut is up to the application.
string
Text between sequential shortcut steps. Defaults to then.
ReactNode
Icon rendered before the content.
CSSProperties
Inline styles applied to the row.
boolean
Whether the popup is initially open when uncontrolled.
string
ID of the Page shown when the popup opens. Closing the popup resets the page history to it. Defaults to root.
boolean
Default for standalone options. Use SelectionGroup.multiple for independently owned selection values.
((open: boolean) => void)
Called with the next open state when the popup opens or closes.
boolean
Whether the popup is open. Use with onOpenChange to control it.
"menu" | "panel" | "picker"
default:"menu"
Interaction model: menu for commands and links, picker for search and selectable options, or panel for forms. It sets the ARIA roles, initial focus, and keyboard navigation.
string
CSS class applied to the row.
number
Delay in milliseconds before a submenu opened on hover closes. Requires openOnHover. Defaults to 0.
"danger" | "neutral"
Color of the text and icons. danger marks a destructive action.
number
Delay in milliseconds before the submenu opens on hover. Requires openOnHover. Defaults to 300.
ReactNode
Supporting text, placed according to descriptionPlacement.
"end" | "inline"
Where the description renders: inline after the content or at the end of the row.
ReactNode
Icon rendered after the content.
boolean
default:"false"
Whether the button should be focusable when disabled.
boolean
default:"true"
Shows a chevron indicating that the item opens a submenu.
boolean
default:"true when render is omitted; false otherwise"
Whether the component renders a native <button> element when replacing it via the render prop. Set to false if the rendered element is not a button (for example, <div>).
boolean
default:"true"
Also opens the submenu when the row is hovered.
ReactElement<unknown, string | JSXElementConstructor<any>>
Element rendered as the row instead of a native button. Custom components must forward the supplied props and ref.
ShortcutDefinition
Keyboard shortcut keys displayed at the end of the row. Registering the shortcut is up to the application.
string
Text between sequential shortcut steps. Defaults to then.
ReactNode
Icon rendered before the content.
CSSProperties
Inline styles applied to the row.
number
Number of grid columns. Sets the grid layout and enables horizontal arrow navigation and vertical movement by row.
ReactNode
Heading displayed above the rows. It also names the group for assistive technologies.
boolean
Caps the section height and scrolls its rows. Nest labelled sections inside it to scroll them as one list.
string
Class applied to the separator. Native div attributes and refs are also accepted.
ReactNode
Loading message announced as a polite, busy status.
string
Class applied to the status. Native div attributes and refs are also accepted.
ReactNode
Empty-state message announced as a polite status.
string
Class applied to the status. Native div attributes and refs are also accepted.