Skip to content
Primitiv home
Framework
Consumption mode

Dropdown

stableSource Figma

A menu-button dropdown built on the native HTML Popover API — a bordered, shadow-defined panel of selectable rows (plain items, checkbox/radio items, submenus), grouped with labels and separators. Positioned with CSS anchor positioning; keyboard, typeahead, and focus are owned by the headless primitive.

Playground

Density

Preview

Size
import { Dropdown, DropdownTrigger, DropdownContent, DropdownItem, DropdownSeparator } from "@/components/ui/dropdown";
<Dropdown>  <DropdownTrigger asChild>    <button style={{ anchorName: "--menu" }}>Actions</button>  </DropdownTrigger>  <DropdownContent size="md" style={{ positionAnchor: "--menu" }}>    <DropdownItem>Edit</DropdownItem>    <DropdownItem>Duplicate</DropdownItem>    <DropdownSeparator />    <DropdownItem>Delete</DropdownItem>  </DropdownContent></Dropdown>

Density is set by a data-density ancestor — the Context system, not a Dropdown prop.

Installation

npx primitiv add dropdown

Import

import { Dropdown } from "@/components/ui/dropdown";

Copied into your project as .primitiv-dropdown — you own the file afterwards, so upgrades are opt-in.

Headless mode installs the npm package instead: @primitiv-ui/react

Anatomy

<Dropdown>  <DropdownTrigger />  <DropdownContent>    <DropdownItem />    <DropdownSeparator />    <DropdownCheckboxItem><DropdownItemIndicator /></DropdownCheckboxItem>    <DropdownSub>      <DropdownSubTrigger />      <DropdownSubContent />    </DropdownSub>  </DropdownContent></Dropdown>

Props

Dropdown.Root

Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
childrenReactNodeheadlessThe menu's sub-components — typically a single DropdownTriggerProps * Dropdown.Trigger} followed by a DropdownContentProps * Dropdown.Content}. Root renders no DOM of its own; it only provides context to these descendants.
defaultOpenbooleanfalseheadlessOpen state on first render. Omit to start closed. The component owns the state thereafter. Forbidden in controlled mode — use open instead.
dir"ltr" | "rtl"headlessReading direction for the menu. Affects which arrow key opens / closes a submenu — ArrowRight opens in "ltr", ArrowLeft opens in "rtl". Falls back to the inherited DirectionProvider value, or to "ltr" if no provider is present.
onOpenChange(open: boolean) => voidheadlessCalled whenever a user-driven transition opens or closes the menu (trigger click, Escape, outside click, selection). Optional in uncontrolled mode. Called whenever a user-driven transition would open or close the menu. The parent is responsible for reflecting the new value back into open. Required in controlled mode.
openbooleanheadlessForbidden in uncontrolled mode — use defaultOpen instead. The current open state. Must be kept in sync by the parent via onOpenChange; the component never mutates it internally.

Dropdown.Trigger

Extends HTMLButtonElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender the composed child element instead of the default <button type="button">. The trigger's ARIA contract (aria-haspopup, aria-expanded, aria-controls) and its click handler are merged onto the child via the Slot pattern.
childrenReactNodeheadlessThe trigger's visible label (or an element, with asChild).
refRef<HTMLButtonElement>headlessAllows getting a ref to the component instance. Once the component unmounts, React will set ref.current to null (or call the ref with null if you passed a callback ref). Forwarded to the underlying HTMLButtonElement.

Dropdown.Content

Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender the composed child element (with menu semantics) instead of the default <menu role="menu" popover="auto">. The managed role, popover, and id attributes and the keyboard handler are merged onto the child via the Slot pattern.
childrenReactNodeheadlessThe menu items — any mix of DropdownItemProps`Dropdown.Item`, DropdownCheckboxItemProps`Dropdown.CheckboxItem`, DropdownRadioGroupProps`Dropdown.RadioGroup`, DropdownGroupProps`Dropdown.Group`, DropdownSeparatorProps`Dropdown.Separator`, and DropdownSubProps`Dropdown.Sub`.
refRef<HTMLMenuElement>headlessForwarded to the underlying HTMLMenuElement.
size"xs" | "sm" | "md" | "lg" | "xl"mdstyledPanel + row scale; data-density scales the sizing within each size. Re-points every child sizing knob (item height/padding/gap/radius/font, panel radius/padding, label, separator).
placement"bottom-start" | "bottom-end" | "top-start" | "top-end" | "submenu"bottom-startstyledWhich side of the trigger the panel opens on, via CSS position-area. Requires the consumer to wire anchor-name (on the trigger) + position-anchor (on Content). Submenus use the submenu value, which opens to the inline-end side.

Dropdown.Item

Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender the composed child element (with menuitem semantics) instead of the default <li role="menuitem">.
childrenReactNodeheadlessThe item's visible content (or an element, with asChild).
disabledbooleanfalseheadlessMark the item non-interactive. Sets aria-disabled="true" and skips the item during arrow navigation, typeahead, and activation.
onSelect(event: Event) => voidheadlessFires when the item is activated (click, Enter, or Space). Called with a cancellable event whose preventDefault() skips the auto-close that Dropdown performs after selection.
refRef<HTMLLIElement>headlessAllows getting a ref to the component instance. Once the component unmounts, React will set ref.current to null (or call the ref with null if you passed a callback ref). Forwarded to the underlying HTMLLIElement.

Dropdown.ItemLeading

Styled surface only — the copied file adds this region. @primitiv-ui/react exports no such part, so under Headless it is your own element.

Extends HTMLSpanElement — every native attribute of that element is accepted and forwarded.

No props of its own.

Dropdown.ItemLabel

Styled surface only — the copied file adds this region. @primitiv-ui/react exports no such part, so under Headless it is your own element.

Extends HTMLSpanElement — every native attribute of that element is accepted and forwarded.

No props of its own.

Dropdown.ItemTrailing

Styled surface only — the copied file adds this region. @primitiv-ui/react exports no such part, so under Headless it is your own element.

Extends HTMLSpanElement — every native attribute of that element is accepted and forwarded.

No props of its own.

Dropdown.CheckboxItem

Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender the composed child element (with role="menuitemcheckbox") instead of the default <li role="menuitemcheckbox">.
checkedCheckedStateheadlessForbidden in uncontrolled mode — use defaultChecked instead. The current checked state — true, false, or "indeterminate". Must be kept in sync by the parent via onCheckedChange. An indeterminate value resolves to true on the next activation.
childrenReactNodeheadlessThe item's content — typically a DropdownItemIndicatorProps * Dropdown.ItemIndicator} plus a text label.
defaultCheckedCheckedStatefalseheadlessChecked state on first render — true, false, or "indeterminate" (rendered as aria-checked="mixed"). Omit to start unchecked. Forbidden in controlled mode — use checked instead.
disabledbooleanfalseheadlessMark the item non-interactive. Sets aria-disabled="true"; activation is a no-op.
onCheckedChange(checked: boolean) => voidheadlessCalled with the new boolean checked state whenever the user toggles the item. Optional in uncontrolled mode. Called with the new boolean checked state whenever the user toggles the item. Required in controlled mode.
onSelect(event: Event) => voidheadlessFires after activation toggles the checked state, with a cancellable event. Call event.preventDefault() to keep the menu open after toggling — useful for flipping several checkboxes in a row.
refRef<HTMLLIElement>headlessAllows getting a ref to the component instance. Once the component unmounts, React will set ref.current to null (or call the ref with null if you passed a callback ref). Forwarded to the underlying HTMLLIElement.

Dropdown.RadioGroup

Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender the composed child element (with role="group") instead of the default <li role="group"> wrapping <ul role="none">.
childrenReactNodeheadlessThe radio choices — a set of DropdownRadioItemProps * Dropdown.RadioItem} elements.
defaultValuestringheadlessThe item value selected on first render. Omit for no initial selection. Forbidden in controlled mode — use value instead.
onValueChange(value: string) => voidheadlessCalled with the newly selected item value whenever the user picks a different radio item. Optional in uncontrolled mode. Called with the newly selected item value whenever the user picks a different radio item. Required in controlled mode.
refRef<HTMLLIElement>headlessAllows getting a ref to the component instance. Once the component unmounts, React will set ref.current to null (or call the ref with null if you passed a callback ref). Forwarded to the underlying HTMLLIElement.
valuestringheadlessForbidden in uncontrolled mode — use defaultValue instead. The value of the currently selected DropdownRadioItemProps * Dropdown.RadioItem}. Must be kept in sync by the parent via onValueChange.

Dropdown.RadioItem

Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
value (required)stringheadlessThis item's identifier within the enclosing DropdownRadioGroupProps`Dropdown.RadioGroup`. When it matches the group's active value the item is checked. Required.
asChildbooleanfalseheadlessRender the composed child element (with role="menuitemradio") instead of the default <li role="menuitemradio">.
childrenReactNodeheadlessThe item's content — typically a DropdownItemIndicatorProps * Dropdown.ItemIndicator} plus a text label.
disabledbooleanfalseheadlessMark the item non-interactive. Sets aria-disabled="true"; activation is a no-op.
onSelect(event: Event) => voidheadlessFires after activation selects this item, with a cancellable event. Call event.preventDefault() to keep the menu open after selecting.
refRef<HTMLLIElement>headlessAllows getting a ref to the component instance. Once the component unmounts, React will set ref.current to null (or call the ref with null if you passed a callback ref). Forwarded to the underlying HTMLLIElement.

Dropdown.ItemIndicator

Extends HTMLSpanElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender the composed child element instead of the default <span>.
childrenReactNodeheadlessThe mark to render — commonly an SVG check or bullet icon.
forceMountbooleanfalseheadlessRender the indicator even when its parent item is unchecked. The data-state attribute still reflects the live state ("checked" / "unchecked" / "indeterminate"), so consumers can animate the indicator in and out instead of mounting / unmounting it.
refRef<HTMLSpanElement>headlessAllows getting a ref to the component instance. Once the component unmounts, React will set ref.current to null (or call the ref with null if you passed a callback ref). Forwarded to the underlying HTMLSpanElement.

Dropdown.Label

Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender the composed child element instead of the default <li>.
childrenReactNodeheadlessThe label text.
refRef<HTMLLIElement>headlessAllows getting a ref to the component instance. Once the component unmounts, React will set ref.current to null (or call the ref with null if you passed a callback ref). Forwarded to the underlying HTMLLIElement.

Dropdown.Separator

Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender the composed child element (with separator semantics) instead of the default <li role="separator">.
childrenReactNodeheadlessOptional content; separators are usually empty.
refRef<HTMLLIElement>headlessAllows getting a ref to the component instance. Once the component unmounts, React will set ref.current to null (or call the ref with null if you passed a callback ref). Forwarded to the underlying HTMLLIElement.

Dropdown.Group

Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender the composed child element (with role="group") instead of the default <li role="group"> wrapping <ul role="none">.
childrenReactNodeheadlessThe grouped items, optionally led by a DropdownLabelProps * Dropdown.Label} that names the group.
refRef<HTMLLIElement>headlessAllows getting a ref to the component instance. Once the component unmounts, React will set ref.current to null (or call the ref with null if you passed a callback ref). Forwarded to the underlying HTMLLIElement.

Dropdown.Sub

Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
childrenReactNodeheadlessThe submenu boundary's content — a DropdownSubTriggerProps * Dropdown.SubTrigger} and its sibling DropdownSubContentProps * Dropdown.SubContent}.
defaultOpenbooleanfalseheadlessSubmenu open state on first render. Omit to start closed. Forbidden in controlled mode — use open instead.
onOpenChange(open: boolean) => voidheadlessCalled whenever the submenu opens or closes. Optional in uncontrolled mode. Called whenever the submenu would open or close. The parent reflects the new value back into open. Required in controlled mode.
openbooleanheadlessForbidden in uncontrolled mode — use defaultOpen instead. The current submenu open state. Must be kept in sync by the parent via onOpenChange.

Dropdown.SubTrigger

Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender the composed child element (with menuitem + submenu-trigger semantics) instead of the default <li role="menuitem">.
childrenReactNodeheadlessThe trigger's visible label (or an element, with asChild).
disabledbooleanfalseheadlessMark the trigger non-interactive. Sets aria-disabled="true" and ignores both click and the open arrow key.
refRef<HTMLLIElement>headlessAllows getting a ref to the component instance. Once the component unmounts, React will set ref.current to null (or call the ref with null if you passed a callback ref). Forwarded to the underlying HTMLLIElement.

Dropdown.SubContent

Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender the composed child element (with menu semantics) instead of the default <menu role="menu" popover="auto">.
childrenReactNodeheadlessThe submenu's items.
refRef<HTMLMenuElement>headlessForwarded to the underlying HTMLMenuElement.

Styling contract

Control frame

--primitiv-dropdown-surface--primitiv-dropdown-fg--primitiv-dropdown-border-width--primitiv-dropdown-border-color--primitiv-dropdown-shadow--primitiv-dropdown-radius--primitiv-dropdown-padding-block--primitiv-dropdown-padding-inline--primitiv-dropdown-min-inline-size--primitiv-dropdown-offset--primitiv-dropdown-row-inset--primitiv-dropdown-label-height--primitiv-dropdown-label-padding-inline--primitiv-dropdown-label-color--primitiv-dropdown-label-font-family--primitiv-dropdown-label-font-size--primitiv-dropdown-label-font-weight--primitiv-dropdown-label-line-height--primitiv-dropdown-label-letter-spacing--primitiv-dropdown-max-block-size--primitiv-dropdown-viewport-margin

Item

--primitiv-dropdown-item-height--primitiv-dropdown-item-padding-inline--primitiv-dropdown-item-gap--primitiv-dropdown-item-radius--primitiv-dropdown-item-font-size--primitiv-dropdown-item-line-height--primitiv-dropdown-item-icon-size--primitiv-dropdown-item-font-family--primitiv-dropdown-item-font-weight--primitiv-dropdown-item-color--primitiv-dropdown-item-bg--primitiv-dropdown-item-bg-highlighted

Group label & separator

--primitiv-dropdown-separator-spacing--primitiv-dropdown-separator-color--primitiv-dropdown-separator-thickness

Keyboard

KeyBehaviour
Enter / SpaceOpen the menu from the trigger; activate the focused item.
ArrowDown / ArrowUpMove focus to the next / previous item, wrapping at the ends.
ArrowRightOpen the focused submenu (or, on dir="rtl", the mirror).
ArrowLeftClose the current submenu and return to its trigger.
Home / EndFirst / last item.
EscapeClose the menu and return focus to the trigger.

Data attributes

DropdownContent

className: .primitiv-dropdown

AttributeValueWhen
data-stateopen | closedopen / closed

DropdownItem

className: .primitiv-dropdown__item

AttributeValueWhen
data-highlighted""the cursor is on this row — set on pointer-enter and by keyboard navigation alike

Accessibility

  • It is the WAI-ARIA Menu pattern: the menu is a single tab stop with roving focus, so the arrow keys move between items and Tab leaves the whole menu — a keyboard user is not made to Tab through every row.
  • The panel is a native [popover] in the top layer, so it escapes overflow: hidden and z-index ancestors, and light-dismisses on an outside click. Focus moves into the menu on open and back to the trigger on close.
  • Dropdown.CheckboxItem and Dropdown.RadioItem carry the real menuitemcheckbox/menuitemradio roles and checked state, so their on/off is announced — the Dropdown.ItemIndicator is the visual half of a state assistive tech already conveys.
  • Dropdown.Label names its Dropdown.Group, so items are announced within their section rather than as one long undifferentiated list.
  • A Dropdown.Item supports disabled, which keeps the row focusable (so a screen-reader user learns it exists) while skipping it in activation — a disabled row that vanishes from the keyboard is one nobody discovers.

Examples

A menu of actions

Click the trigger to open a list of actions. Each Dropdown.Item runs its onSelect and closes the menu; Dropdown.Separator groups related runs. Add a leading icon or a trailing shortcut with Dropdown.ItemLeading / Dropdown.ItemTrailing inside the item.

Density
import { Dropdown, DropdownTrigger, DropdownContent, DropdownItem, DropdownSeparator } from "@/components/ui/dropdown";
<Dropdown>  <DropdownTrigger asChild>    <button style={{ anchorName: "--menu" }}>Actions</button>  </DropdownTrigger>  <DropdownContent style={{ positionAnchor: "--menu" }}>    <DropdownItem onSelect={() => edit()}>Edit</DropdownItem>    <DropdownItem onSelect={() => duplicate()}>Duplicate</DropdownItem>    <DropdownSeparator />    <DropdownItem onSelect={() => remove()}>Delete</DropdownItem>  </DropdownContent></Dropdown>

Checkbox and radio items

Dropdown.CheckboxItem is a togglable row (many can be on); Dropdown.RadioGroup + Dropdown.RadioItem is a single-choice set. Each carries a Dropdown.ItemIndicator that shows only when the row is selected — the check or dot in the gutter. Selecting one keeps the menu open for these, unlike a plain action.

Density
  • Editor
  • Word wrap
  • Theme
    • System
    • Light
    • Dark
  • import { Dropdown, DropdownContent, DropdownCheckboxItem, DropdownRadioGroup, DropdownRadioItem, DropdownItemIndicator, DropdownLabel, DropdownSeparator } from "@/components/ui/dropdown";
    <DropdownCheckboxItem checked={wrap} onCheckedChange={setWrap}>  <DropdownItemIndicator><Check /></DropdownItemIndicator>  Word wrap</DropdownCheckboxItem>
    <DropdownRadioGroup value={theme} onValueChange={setTheme}>  <DropdownRadioItem value="light">    <DropdownItemIndicator><Check /></DropdownItemIndicator>Light  </DropdownRadioItem></DropdownRadioGroup>

    Dropdown.Sub nests a second menu: Dropdown.SubTrigger is a row that opens Dropdown.SubContent to the side on hover or ArrowRight. A submenu is a real anchor-positioned menu of its own, so it flips when there is not room — use it to keep a long list of options tucked behind one row rather than sprawling.

    Density
    import { Dropdown, DropdownContent, DropdownItem, DropdownSub, DropdownSubTrigger, DropdownSubContent } from "@/components/ui/dropdown";
    <DropdownSub>  <DropdownSubTrigger>Move to…</DropdownSubTrigger>  <DropdownSubContent>    <DropdownItem>Project A</DropdownItem>    <DropdownItem>Project B</DropdownItem>  </DropdownSubContent></DropdownSub>

    Labelled groups

    Dropdown.Group with a Dropdown.Label sections a long menu into named runs, and the label is wired as the group's accessible name — so a screen reader announces which group each item belongs to, not just a flat list.

    Density
    • Account
    • Workspace
  • import { Dropdown, DropdownContent, DropdownGroup, DropdownLabel, DropdownItem } from "@/components/ui/dropdown";
    <DropdownGroup>  <DropdownLabel>Account</DropdownLabel>  <DropdownItem>Profile</DropdownItem>  <DropdownItem>Billing</DropdownItem></DropdownGroup>