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.
Density is set by a data-density ancestor — the Context system, not a Dropdown prop.
Installation
npx primitiv add dropdown
pnpm dlx primitiv add dropdown
yarn dlx primitiv add dropdown
bunx 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.Root owns the open state; Dropdown.Trigger is the menu button; Dropdown.Content is the panel. Inside it: Dropdown.Item for actions, Dropdown.CheckboxItem / Dropdown.RadioGroup + Dropdown.RadioItem for toggles (each with a Dropdown.ItemIndicator for the check/dot), Dropdown.Label + Dropdown.Group to section it, Dropdown.Separator between runs, and Dropdown.Sub / Dropdown.SubTrigger / Dropdown.SubContent for a nested submenu. Positioning is CSS anchor positioning you wire (anchor-name ↔ position-anchor).
Generated from source — headless rows from each *Props type’s JSDoc, styled rows from contract.json. Never hand-maintained.
Dropdown.Root
Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
From
Description
children
ReactNode
—
headless
The 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.
defaultOpen
boolean
false
headless
Open state on first render. Omit to start closed. The component owns
the state thereafter.
Forbidden in controlled mode — use open instead.
dir
"ltr" | "rtl"
—
headless
Reading 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) => void
—
headless
Called 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.
open
boolean
—
headless
Forbidden 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.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render 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.
children
ReactNode
—
headless
The trigger's visible label (or an element, with asChild).
ref
Ref<HTMLButtonElement>
—
headless
Allows 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.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render 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.
children
ReactNode
—
headless
The menu items — any mix of DropdownItemProps`Dropdown.Item`,
DropdownCheckboxItemProps`Dropdown.CheckboxItem`,
DropdownRadioGroupProps`Dropdown.RadioGroup`,
DropdownGroupProps`Dropdown.Group`,
DropdownSeparatorProps`Dropdown.Separator`, and
DropdownSubProps`Dropdown.Sub`.
ref
Ref<HTMLMenuElement>
—
headless
Forwarded to the underlying HTMLMenuElement.
size
"xs" | "sm" | "md" | "lg" | "xl"
md
styled
Panel + 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).
Which 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.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render the composed child element (with menuitem semantics) instead of
the default <li role="menuitem">.
children
ReactNode
—
headless
The item's visible content (or an element, with asChild).
disabled
boolean
false
headless
Mark the item non-interactive. Sets aria-disabled="true" and skips
the item during arrow navigation, typeahead, and activation.
onSelect
(event: Event) => void
—
headless
Fires 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.
ref
Ref<HTMLLIElement>
—
headless
Allows 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.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render the composed child element (with role="menuitemcheckbox")
instead of the default <li role="menuitemcheckbox">.
checked
CheckedState
—
headless
Forbidden 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.
children
ReactNode
—
headless
The item's content — typically a DropdownItemIndicatorProps * Dropdown.ItemIndicator} plus a text label.
defaultChecked
CheckedState
false
headless
Checked state on first render — true, false, or "indeterminate"
(rendered as aria-checked="mixed"). Omit to start unchecked.
Forbidden in controlled mode — use checked instead.
disabled
boolean
false
headless
Mark the item non-interactive. Sets aria-disabled="true"; activation
is a no-op.
onCheckedChange
(checked: boolean) => void
—
headless
Called 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) => void
—
headless
Fires 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.
ref
Ref<HTMLLIElement>
—
headless
Allows 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.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render the composed child element (with role="group") instead of the
default <li role="group"> wrapping <ul role="none">.
children
ReactNode
—
headless
The radio choices — a set of DropdownRadioItemProps * Dropdown.RadioItem} elements.
defaultValue
string
—
headless
The item value selected on first render. Omit for no initial
selection.
Forbidden in controlled mode — use value instead.
onValueChange
(value: string) => void
—
headless
Called 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.
ref
Ref<HTMLLIElement>
—
headless
Allows 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.
value
string
—
headless
Forbidden 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.
Prop
Type
Default
From
Description
value* (required)
string
—
headless
This item's identifier within the enclosing
DropdownRadioGroupProps`Dropdown.RadioGroup`. When it matches
the group's active value the item is checked. Required.
asChild
boolean
false
headless
Render the composed child element (with role="menuitemradio") instead
of the default <li role="menuitemradio">.
children
ReactNode
—
headless
The item's content — typically a DropdownItemIndicatorProps * Dropdown.ItemIndicator} plus a text label.
disabled
boolean
false
headless
Mark the item non-interactive. Sets aria-disabled="true"; activation
is a no-op.
onSelect
(event: Event) => void
—
headless
Fires after activation selects this item, with a cancellable event.
Call event.preventDefault() to keep the menu open after selecting.
ref
Ref<HTMLLIElement>
—
headless
Allows 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.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render the composed child element instead of the default <span>.
children
ReactNode
—
headless
The mark to render — commonly an SVG check or bullet icon.
forceMount
boolean
false
headless
Render 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.
ref
Ref<HTMLSpanElement>
—
headless
Allows 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.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render the composed child element instead of the default <li>.
children
ReactNode
—
headless
The label text.
ref
Ref<HTMLLIElement>
—
headless
Allows 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.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render the composed child element (with separator semantics) instead of
the default <li role="separator">.
children
ReactNode
—
headless
Optional content; separators are usually empty.
ref
Ref<HTMLLIElement>
—
headless
Allows 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.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render the composed child element (with role="group") instead of the
default <li role="group"> wrapping <ul role="none">.
children
ReactNode
—
headless
The grouped items, optionally led by a DropdownLabelProps * Dropdown.Label} that names the group.
ref
Ref<HTMLLIElement>
—
headless
Allows 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.
Prop
Type
Default
From
Description
children
ReactNode
—
headless
The submenu boundary's content — a DropdownSubTriggerProps * Dropdown.SubTrigger} and its sibling DropdownSubContentProps * Dropdown.SubContent}.
defaultOpen
boolean
false
headless
Submenu open state on first render. Omit to start closed.
Forbidden in controlled mode — use open instead.
onOpenChange
(open: boolean) => void
—
headless
Called 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.
open
boolean
—
headless
Forbidden 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.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render the composed child element (with menuitem + submenu-trigger
semantics) instead of the default <li role="menuitem">.
children
ReactNode
—
headless
The trigger's visible label (or an element, with asChild).
disabled
boolean
false
headless
Mark the trigger non-interactive. Sets aria-disabled="true" and
ignores both click and the open arrow key.
ref
Ref<HTMLLIElement>
—
headless
Allows 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.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render the composed child element (with menu semantics) instead of the
default <menu role="menu" popover="auto">.
children
ReactNode
—
headless
The submenu's items.
ref
Ref<HTMLMenuElement>
—
headless
Forwarded to the underlying HTMLMenuElement.
Styling contract
36 CSS custom properties on .primitiv-dropdown — mode-agnostic. These names are the stable surface; the values are not.
The menu is a roving-focus list: it opens focused on the first item, and the arrow keys move between items (a single tab stop). It is the WAI-ARIA Menu pattern — a keyboard user does not Tab through every row.
Key
Behaviour
Enter / Space
Open the menu from the trigger; activate the focused item.
ArrowDown / ArrowUp
Move focus to the next / previous item, wrapping at the ends.
ArrowRight
Open the focused submenu (or, on dir="rtl", the mirror).
ArrowLeft
Close the current submenu and return to its trigger.
Home / End
First / last item.
Escape
Close the menu and return focus to the trigger.
Data attributes
Emitted automatically by the headless primitive — style against these rather than adding your own state classes. Grouped by the part that emits them, since most are emitted by more than one.
DropdownContent
className:.primitiv-dropdown
Attribute
Value
When
data-state
open | closed
open / closed
Dropdown.Content
Attribute
Value
When
data-state
open | closed
open / closed
DropdownItem
className:.primitiv-dropdown__item
Attribute
Value
When
data-highlighted
""
the cursor is on this row — set on pointer-enter and by keyboard navigation alike
Dropdown.Item
Attribute
Value
When
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
Every example below reacts to the density control. Currently showing Styled mode.
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.
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.
import{Dropdown,DropdownContent,DropdownCheckboxItem,DropdownRadioGroup,DropdownRadioItem,DropdownItemIndicator,DropdownLabel,DropdownSeparator}from"@/components/ui/dropdown";<DropdownCheckboxItemchecked={wrap}onCheckedChange={setWrap}><DropdownItemIndicator><Check /></DropdownItemIndicator> Word wrap</DropdownCheckboxItem><DropdownRadioGroupvalue={theme}onValueChange={setTheme}><DropdownRadioItemvalue="light"><DropdownItemIndicator><Check /></DropdownItemIndicator>Light</DropdownRadioItem></DropdownRadioGroup>
import{Dropdown}from"@primitiv-ui/react";<Dropdown.CheckboxItemchecked={wrap}onCheckedChange={setWrap}><Dropdown.ItemIndicator><Check /></Dropdown.ItemIndicator> Word wrap</Dropdown.CheckboxItem><Dropdown.RadioGroupvalue={theme}onValueChange={setTheme}><Dropdown.RadioItemvalue="light"><Dropdown.ItemIndicator><Check /></Dropdown.ItemIndicator>Light</Dropdown.RadioItem></Dropdown.RadioGroup>
A submenu
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.
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.