Dropdown Menu

A contextual menu that opens on trigger. Built on Radix DropdownMenu for keyboard navigation, focus management, and a11y; animated with interruptible enter/exit transitions.

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add dropdown-menu

Usage#

usage.tsxTSX
import {  DropdownMenu, DropdownMenuTrigger, DropdownMenuContent,  DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator,  DropdownMenuShortcut,} from "@/components/ui/dropdown-menu"import { Button } from "@/components/ui/button"import { GearSix as Gear, SignOut, UserCircle } from "koala-icons"export function Example() {  return (    <DropdownMenu>      <DropdownMenuTrigger asChild>        <Button variant="outline">My account</Button>      </DropdownMenuTrigger>      <DropdownMenuContent className="w-48">        <DropdownMenuLabel>My account</DropdownMenuLabel>        <DropdownMenuSeparator />        <DropdownMenuItem>          <UserCircle /> Profile          <DropdownMenuShortcut>⌘P</DropdownMenuShortcut>        </DropdownMenuItem>        <DropdownMenuItem>          <Gear /> Settings          <DropdownMenuShortcut>⌘,</DropdownMenuShortcut>        </DropdownMenuItem>        <DropdownMenuSeparator />        <DropdownMenuItem variant="destructive">          <SignOut /> Log out        </DropdownMenuItem>      </DropdownMenuContent>    </DropdownMenu>  )}

Anatomy#

import {  DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuLabel,  DropdownMenuGroup, DropdownMenuItem, DropdownMenuItemText, DropdownMenuItemDescription,  DropdownMenuShortcut, DropdownMenuCheckboxItem, DropdownMenuRadioGroup,  DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub,  DropdownMenuSubTrigger, DropdownMenuSubContent,} from "@/components/ui/dropdown-menu"<DropdownMenu>  <DropdownMenuTrigger />  <DropdownMenuContent>    <DropdownMenuLabel />    <DropdownMenuGroup>      <DropdownMenuItem>        <DropdownMenuItemText>          <DropdownMenuItemDescription />        </DropdownMenuItemText>        <DropdownMenuShortcut />      </DropdownMenuItem>    </DropdownMenuGroup>    <DropdownMenuCheckboxItem />    <DropdownMenuRadioGroup>      <DropdownMenuRadioItem />    </DropdownMenuRadioGroup>    <DropdownMenuSeparator />    <DropdownMenuSub>      <DropdownMenuSubTrigger />      <DropdownMenuSubContent />    </DropdownMenuSub>  </DropdownMenuContent></DropdownMenu>

Avatar trigger#

Any element can be the trigger via asChild. A common pattern is a header avatar that opens an account menu, with the signed-in identity repeated as a non-interactive label at the top.

Icon button trigger#

A ghost iconOnly button makes a compact “more actions” menu for table rows and card headers. Align the content to the trigger edge with align="end".

Keyboard shortcuts#

DropdownMenuShortcut renders the small gray Kbd chip at the row's end. It is purely decorative: wire up the actual key binding in your app with a global listener.

Checkbox items#

Switch items#

For a setting that applies the moment it flips, pass variant="switch" to a checkbox item. The row draws a switch on the same line as the shortcut and the check, and the menu stays open when it is toggled (closeOnSelect is off for switches), so several can be set in one visit. The row is still a menu checkbox with its checked state announced; the switch is its drawing, not a second control.

A row that navigates stays a menu item: wrap the link with asChild, so it keeps the row’s styling, arrow-key focus and Enter to activate, while Next.js client navigation and Cmd/Ctrl-click keep working. Mark a link that leaves the site with a trailing ArrowUpRight at size-4.

Descriptions#

A navigation menu often needs more than a label: a title over a sentence that says where the row goes. Put both in a DropdownMenuItemText after the leading icon, the sentence in a DropdownMenuItemDescription. A described row pins its icon to the title’s line instead of centering it on the block, and the sentence steps back by color and weight at the same size, so the title keeps the hierarchy. Give the content a wider measure (w-80) so the sentences wrap readably. Both parts are optional: a row without them stays a plain item.

Radio items#

By default a sub-menu is nested: it flies out beside its row. Pass submenu="navigation" and it drills in place instead: its rows replace the menu's behind a Back row, and the surface resizes to fit. Use it for a deep menu, or one with no room beside it. The parts and the rows are the same, and so are the keys: → goes in, and ← or Esc steps back one layer before the menu closes. It is the Dock's panel, opened from a trigger.

API reference#

DropdownMenu

The root, Radix DropdownMenu.Root; renders no DOM. Every part that renders an element accepts className, merged last.

PropTypeDefaultDescription
openboolean-Controlled open state.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => void-Called when the menu opens or closes.
modalbooleantrueBlock pointer interaction with the page and trap focus while open.
dir"ltr" | "rtl"-Reading direction; flips sub-menus and arrow keys.

DropdownMenuTrigger

Opens the menu, Radix DropdownMenu.Trigger. Renders a <button> and forwards all its props.

PropTypeDefaultDescription
asChildbooleanfalseMerge onto your own element (a Button, an avatar button) instead of rendering a <button>.
Data attributeDescription
data-state"open" or "closed".
data-disabledPresent when the trigger is disabled.

DropdownMenuContent

The menu surface, Radix DropdownMenu.Content portalled to the body (or to the full-screen element); forwards all its props.

PropTypeDefaultDescription
submenu"nested" | "navigation""nested"How every DropdownMenuSub opens: a flyout beside its row, or a layer that replaces the rows in place behind a Back row.
side"top" | "right" | "bottom" | "left""bottom"Preferred side of the trigger; the menu flips when there is no room.
align"start" | "center" | "end""center"Alignment against the trigger.
sideOffsetnumber6Gap in pixels between the trigger and the menu.
alignOffsetnumber0Shift in pixels along the alignment axis.
collisionPaddingnumber0Distance in pixels kept from the viewport edge.
loopbooleanfalseArrow keys wrap from the last row to the first.
containerHTMLElement | null-Portal target. Defaults to the body, or the element that is full screen when the menu opens.
density"comfortable" | "compact""compact"Row height for every row inside, 36px or 32px. Unset, the rows follow the nearest DensityProvider.
onCloseAutoFocus(event: Event) => void-Called as focus returns to the trigger on close; preventDefault() to send it elsewhere.
onEscapeKeyDown(event: KeyboardEvent) => void-Called on Escape; in navigation mode Escape first steps back a layer.
onInteractOutside(event: Event) => void-Called on a pointer down or focus outside the menu; preventDefault() keeps it open.
Data attributeDescription
data-state"open" or "closed".
data-sideThe side the menu actually opened on, after collisions.
data-align"start", "center" or "end".
data-submenu"nested" or "navigation".
CSS variableDescription
--surfaceSet to var(--popover), read by nested Inputs and Selects.
--radix-dropdown-menu-trigger-widthThe trigger's width, for a menu that matches it (w-(--radix-dropdown-menu-trigger-width)).
--radix-dropdown-menu-content-available-heightHeight left between the trigger and the viewport edge, for capping a long menu.
--radix-dropdown-menu-content-transform-originThe trigger-side origin, for a custom entrance.

DropdownMenuItem

A row, Radix DropdownMenu.Item (a <div role="menuitem">); forwards all its props. Put a leading icon first and a DropdownMenuShortcut last.

PropTypeDefaultDescription
variant"default" | "destructive""default"Destructive tints the label, the leading icon and the highlight red.
insetbooleanfalseIndent the text to line up with icon-bearing siblings.
onSelect(event: Event) => void-Called when the row is chosen; preventDefault() keeps the menu open.
disabledbooleanfalseDims the row and skips it in keyboard navigation.
textValuestring-Text used for typeahead when the row's content is not plain text.
asChildbooleanfalseMerge onto your own element, e.g. a next/link, keeping the row's styling and keys.
density"comfortable" | "compact""compact"Row height, 36px or 32px. Falls back to the menu's density, then the nearest DensityProvider.
Data attributeDescription
data-highlightedPresent on the row under the pointer or keyboard focus.
data-disabledPresent when the row is disabled.
data-insetPresent on an inset row.

DropdownMenuItemText

The title and description column of a described row, placed after the leading icon. Forwards all native <span> props.

DropdownMenuItemDescription

The muted sentence under a row's title, inside DropdownMenuItemText; the leading icon then pins to the title's line. Forwards all native <span> props.

DropdownMenuCheckboxItem

A toggle row, Radix DropdownMenu.CheckboxItem (role="menuitemcheckbox"); forwards all its props.

PropTypeDefaultDescription
checkedboolean | "indeterminate"-The checked state.
onCheckedChange(checked: boolean) => void-Called when the row is toggled.
variant"check" | "switch""check"An ink check on the right, or a mini Switch on the same line for a setting that applies at once.
closeOnSelectbooleantrueClose the menu on toggle. Defaults to false when variant="switch".
disabledbooleanfalseDims the row and skips it in keyboard navigation.
density"comfortable" | "compact""compact"Row height, 36px or 32px. Falls back to the menu's density, then the nearest DensityProvider.
Data attributeDescription
data-state"checked", "unchecked" or "indeterminate".
data-highlightedPresent on the row under the pointer or keyboard focus.
data-disabledPresent when the row is disabled.

DropdownMenuRadioGroup

Groups radio rows into one exclusive choice, Radix DropdownMenu.RadioGroup. Forwards all native <div> props.

PropTypeDefaultDescription
valuestring-The selected value.
onValueChange(value: string) => void-Called when another row is chosen.

DropdownMenuRadioItem

One choice in a DropdownMenuRadioGroup, Radix DropdownMenu.RadioItem; shows the same ink check as a checkbox row. Forwards all its props.

PropTypeDefaultDescription
value*string-The value this row selects.
disabledbooleanfalseDims the row and skips it in keyboard navigation.
density"comfortable" | "compact""compact"Row height, 36px or 32px. Falls back to the menu's density, then the nearest DensityProvider.
Data attributeDescription
data-state"checked" or "unchecked".
data-highlightedPresent on the row under the pointer or keyboard focus.
data-disabledPresent when the row is disabled.

DropdownMenuLabel

A muted caption over a run of rows, Radix DropdownMenu.Label; not focusable. Forwards all native <div> props.

PropTypeDefaultDescription
insetbooleanfalseIndent to line up with icon-bearing rows.
density"comfortable" | "compact""compact"Row height, 36px or 32px. Falls back to the menu's density, then the nearest DensityProvider.

DropdownMenuGroup

Groups related rows (role="group"), Radix DropdownMenu.Group. Forwards all native <div> props.

DropdownMenuSeparator

A hairline between groups that stops at the ring's inner edge, Radix DropdownMenu.Separator. Forwards all native <div> props.

PropTypeDefaultDescription
density"comfortable" | "compact""compact"Row height, 36px or 32px. Falls back to the menu's density, then the nearest DensityProvider.

DropdownMenuShortcut

A display-only key hint pushed to the row's end: the small gray Kbd chip. Forwards all Kbd props; it binds no key.

DropdownMenuSub

Pairs a DropdownMenuSubTrigger with the DropdownMenuSubContent it opens, Radix DropdownMenu.Sub; renders no DOM.

PropTypeDefaultDescription
openboolean-Controlled open state. Ignored in navigation mode, where the menu owns the open branch.
defaultOpenbooleanfalseInitial open state when uncontrolled; nested mode only.
onOpenChange(open: boolean) => void-Called when the sub-menu opens or closes; nested mode only.

DropdownMenuSubTrigger

The row that opens a sub-menu, with a trailing caret; Radix DropdownMenu.SubTrigger, or a plain item in navigation mode. Forwards all its props.

PropTypeDefaultDescription
insetbooleanfalseIndent the text to line up with icon-bearing siblings.
disabledbooleanfalseDims the row and skips it in keyboard navigation.
textValuestring-Text used for typeahead.
density"comfortable" | "compact""compact"Row height, 36px or 32px. Falls back to the menu's density, then the nearest DensityProvider.
Data attributeDescription
data-state"open" or "closed" (nested mode).
data-highlightedPresent on the row under the pointer or keyboard focus.
data-disabledPresent when the row is disabled.

DropdownMenuSubContent

The sub-menu, Radix DropdownMenu.SubContent: a surface beside its row in nested mode, or rows that take the menu's place in navigation mode (where className and the positioning props do not apply).

PropTypeDefaultDescription
backLabelstring"Back"Navigation mode: label of the row that returns to the parent layer.
sideOffsetnumber0Gap in pixels between the row and the flyout.
alignOffsetnumber0Shift in pixels along the row.
loopbooleanfalseArrow keys wrap from the last row to the first.
density"comfortable" | "compact""compact"Row height, 36px or 32px. Falls back to the menu's density, then the nearest DensityProvider.
Data attributeDescription
data-state"open" or "closed".
data-sideThe side the flyout opened on.

DropdownMenuPortal

Radix DropdownMenu.Portal, exported for custom compositions; DropdownMenuContent already portals itself.

PropTypeDefaultDescription
containerHTMLElement | nulldocument.bodyWhere the portal mounts.
forceMounttrue-Keep the children mounted while closed.

FAQ#

Use DropdownMenu for a list of actions or commands fired from a trigger, like a row's more-actions menu or an account menu. Reach for Select when the user is choosing a value to submit in a form; the menu's items run callbacks rather than holding a form value.