Components
Dropdown Menu
Displays a menu of actions or options triggered by a button, with full support for icons, shortcuts, checkbox and radio items, and nested submenus.
Basic usage
Wrap a trigger and a content panel of items. Use asChild to render the trigger as a Button.
Checkbox & radio items
Use DropdownMenuCheckboxItem for independent toggles and DropdownMenuRadioGroup for mutually exclusive choices.
Both are controlled only. The Radix menu primitives read checked (defaulting to false) and value; unlike the standalone Checkbox and RadioGroup they have no defaultChecked or defaultValue. Passing one type-checks — React allows those attributes on any element — but it lands on the underlying <div> and the row never ticks. Hold the state yourself and pass onCheckedChange / onValueChange.
Submenus
Nest a DropdownMenuSub to group related actions behind a hover-expandable trigger.
The package renders DropdownMenuSubContent inside its own portal. That is not cosmetic: DropdownMenuContent carries overflow-x-hidden overflow-y-auto so a long menu can scroll, and its open animation leaves a transform in place, which makes the panel a containing block even for position: fixed children. Without the portal the submenu opens beside the parent panel and is clipped away entirely — the chevron row reacts to nothing, with no error and no warning.
Platform-aware shortcuts
DropdownMenuShortcut is a plain right-aligned span, so whatever glyph you type is what every user sees — ⌘ is simply wrong on Windows and Linux. Drop KeyboardHint into the row instead and write mod for the primary modifier: it renders ⌘ on macOS and Ctrl everywhere else. It reports non-Mac on the first render so server and client HTML match, then corrects itself after hydration.
Compact density
Menu rows measure their vertical padding with py-(--menu-item-py) rather than a fixed py-1.5, so they follow the density axis: 6px at the default setting, 4px under data-density="compact". Only the vertical axis moves — compact means more rows on screen, not text jammed against the panel edge. In an app the attribute is set once on <html> by DensityProvider from @comitor/ui/shell; the panels below scope it by hand so the two can sit side by side.
Props
The parts exported from @comitor/ui. Every part forwards the props of the Radix primitive it wraps, plus the native props of the element it renders.
| Prop | Type | Default | Description |
|---|---|---|---|
DropdownMenu | DropdownMenuPrimitive.Root | — | Root wrapper that owns the open state. |
DropdownMenuTrigger | DropdownMenuPrimitive.Trigger | — | Element that opens the menu. Pass asChild to render your own Button. |
DropdownMenuContent | DropdownMenuPrimitive.Content | — | The floating panel. Always portalled to document.body by the package. |
DropdownMenuPortal | DropdownMenuPrimitive.Portal | — | Escape hatch for portalling content yourself — Content and SubContent already portal, so you rarely need it. |
DropdownMenuGroup | DropdownMenuPrimitive.Group | — | Groups related items so assistive tech announces them together. |
DropdownMenuItem | DropdownMenuPrimitive.Item | — | A selectable action row. See the item props below. |
DropdownMenuCheckboxItem | DropdownMenuPrimitive.CheckboxItem | — | Toggle row with a check indicator. Controlled only — checked (default false) plus onCheckedChange; there is no defaultChecked. |
DropdownMenuRadioGroup | DropdownMenuPrimitive.RadioGroup | — | Groups radio items and tracks the selected value. Controlled only — value plus onValueChange; there is no defaultValue. |
DropdownMenuRadioItem | DropdownMenuPrimitive.RadioItem | — | Single-choice row with a dot indicator; requires value. |
DropdownMenuLabel | DropdownMenuPrimitive.Label | — | Non-interactive section heading. Accepts inset. |
DropdownMenuSeparator | DropdownMenuPrimitive.Separator | — | Full-bleed divider drawn with the decorative border token. |
DropdownMenuShortcut | span | — | Muted, right-aligned shortcut text. Use KeyboardHint instead when the combo contains a modifier. |
DropdownMenuSub | DropdownMenuPrimitive.Sub | — | Wraps a nested submenu (SubTrigger + SubContent). |
DropdownMenuSubTrigger | DropdownMenuPrimitive.SubTrigger | — | Row that opens a submenu; accepts inset and appends a chevron. |
DropdownMenuSubContent | DropdownMenuPrimitive.SubContent | — | The submenu panel, portalled so the parent panel cannot clip it. |
DropdownMenu (root)
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controlled open state of the menu. Omit for uncontrolled behavior. |
defaultOpen | boolean | false | Open state when initially rendered, for uncontrolled usage. |
onOpenChange | (open: boolean) => void | — | Event handler called when the open state changes. |
modal | boolean | true | Whether the menu traps focus and blocks outside interaction while open. |
dir | 'ltr' | 'rtl' | 'ltr' | Reading direction, affecting submenu placement and arrow keys. |
DropdownMenuContent
| Prop | Type | Default | Description |
|---|---|---|---|
align | 'start' | 'center' | 'end' | 'center' | Alignment of the content against the trigger. |
side | 'top' | 'right' | 'bottom' | 'left' | 'bottom' | Preferred side of the trigger to render against. |
sideOffset | number | 4 | Distance in pixels between the content and the trigger. The package raises the Radix default of 0 to 4. |
alignOffset | number | 0 | Offset along the alignment axis, in pixels. |
loop | boolean | false | Whether keyboard navigation wraps around from the last item to the first. |
className | string | — | Additional Tailwind classes — commonly a width such as w-56. |
DropdownMenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'default' | 'destructive' | 'default' | Styles the item — destructive paints the label and icon with the destructive ink token; the focus row tints with the destructive fill at bg-destructive/10 (bg-destructive/20 in dark). |
inset | boolean | false | Adds left padding to align with items that have a leading icon or indicator. |
disabled | boolean | false | Disables the item, removing pointer events and dimming it. |
onSelect | (event: Event) => void | — | Called when the item is selected via mouse or keyboard. |
textValue | string | — | Text used for typeahead matching when the item contains more than plain text. |
asChild | boolean | false | Merges the item props onto its single child — use it to render a link as a menu item. |
Accessibility
- Built on Radix UI, the menu follows the WAI-ARIA menu button pattern with the correct
roleandaria-*wiring applied automatically. - Fully keyboard operable — open with Enter, Space, or arrow keys, move with arrows, activate with Enter, and dismiss with Esc.
- Typeahead lets users jump to an item by typing its label; submenus open with → and close with ←.
- Focus is trapped while the menu is open and returns to the trigger on close; checkbox and radio items expose their checked state to assistive technology.
variant="destructive"paints the label in the destructive ink step, not the destructive fill. The fill is a button background pinned to one value across both themes and only reaches 3.53:1 as text on the popover surface — worse still on focus, where the row tints tobg-destructive/20(2.98:1). The ink step clears both themes and both backgrounds: 7.15:1 / 6.18:1 in light at rest and on focus, 6.42:1 / 5.42:1 in dark.- Keyboard shortcut glyphs are decorative.
KeyboardHintisaria-hiddenfor that reason — symbols such as ⌘⇧↵ are meaningless read aloud, so put the combination in the item's own accessible name when it matters.