Dropdown
A menu hung off a button — icons, shortcuts, checkable rows, radio sets and submenus.
- Source
- 1 file
- Dependencies
- framer-motion
- Playground
- no
Examples
Every variant of the shipped component, live. Switch the layout to read them as a table or one at a time, and open Code for the file behind them.
01Basic
<Dropdown> + <DropdownTrigger asChild> + <DropdownContent>Placement
02Side
<DropdownContent side="top | bottom | left | right" />03Align
<DropdownContent align="start | center | end" />04Offset
<DropdownContent sideOffset={6} />05Width
<DropdownContent matchTriggerWidth />Rows
06Icon
<DropdownItem icon={<CopyIcon />} /> · <DropdownItem shortcut={<LinkIcon />} />07Checkable
SidebarStatus bar
<DropdownCheckboxItem checked onCheckedChange={…} />08One of a set
<DropdownRadioItem value indicator="check | dot" description="…" />09Disabled
<DropdownItem disabled variant="destructive" /> · <Dropdown disabled />Nesting
10Submenus
<DropdownSub> + <DropdownSubTrigger> + <DropdownSubContent>Installation
One package for the whole library — run this once and every component on this site is importable. Adding the tokens to your stylesheet is the other half; the installation guide covers it.
npm install kipuiimport {
Dropdown,
DropdownTrigger,
DropdownContent,
DropdownItem,
DropdownSeparator,
} from "kipui";- Peer dependencies
react 19
react-dom 19
tailwindcss 4
- Bundled dependencies
framer-motion
Usage
dropdown-example.tsx
import {
Dropdown,
DropdownTrigger,
DropdownContent,
DropdownItem,
DropdownSeparator,
} from "kipui";
<Dropdown>
<DropdownTrigger>Options</DropdownTrigger>
<DropdownContent align="end">
<DropdownItem icon={<CopyIcon />} shortcut="⌘C" onSelect={copy}>
Copy link
</DropdownItem>
<DropdownItem onSelect={rename}>Rename</DropdownItem>
<DropdownSeparator />
<DropdownItem variant="destructive" onSelect={remove}>
Delete
</DropdownItem>
</DropdownContent>
</Dropdown>- It opens and closes on the same machinery as Select — measured position, flip when there is no room, tracked while the page scrolls — so a menu near the fold behaves the way the listbox next to it does.
- `side` and `align` are preferences. A menu that cannot fit on the side you asked for is flipped to the other one, and the side it lands on is then held while you scroll rather than flipping back and forth.
- Focus stays on the menu itself and the active row is named by `aria-activedescendant` — so arrow keys, Home/End, Enter, Space and typing the first letters all work without focus ever landing somewhere you cannot see.
- Checkable rows leave the menu open and radio rows close it: toggles come in runs, a choice out of a set is one decision. Both are overridable with `closeOnSelect`.
- `asChild` makes an existing element the trigger — a `Button`, an avatar, an icon button in a table row. It expects exactly one element, and merges its own ref with whatever ref that element already had.
- Submenus open on hover after a short delay and close after a longer one, so crossing a row on the way elsewhere does not open it and travelling to the panel does not close it. → and ← do the same thing from the keyboard.
API reference
Transcribed from the component's own types. Props with no default are required.
Dropdown
| Prop | Default | Description |
|---|---|---|
| openboolean | required | Controlled open state. Leave unset to let the menu own it. |
| defaultOpenboolean | false | Uncontrolled seed. |
| onOpenChange(open: boolean) => void | required | Fires whenever the menu opens or closes, however it was triggered. |
| disabledboolean | false | Blocks the trigger, and closes the menu if it is open. |
| portalTargetHTMLElement | null | required | Where every surface is portalled. Defaults to the body. |
DropdownTrigger
| Prop | Default | Description |
|---|---|---|
| asChildboolean | false | Use the child element as the trigger instead of a button of our own. Expects exactly one element. |
| showChevronboolean | true | The chevron after the label. Ignored under `asChild`. |
DropdownContent
| Prop | Default | Description |
|---|---|---|
| side"top" | "bottom" | "left" | "right" | "bottom" | Preferred side. Flipped only when it does not fit and the other one does. |
| align"start" | "center" | "end" | "start" | Which edge of the menu lines up with the trigger. |
| sideOffsetnumber | 6 | Gap between trigger and menu, in px. |
| alignOffsetnumber | 0 | Shift along the alignment axis, in px. Negative moves towards the start. |
| matchTriggerWidthboolean | false | Menu is at least as wide as its trigger. |
DropdownItem
| Prop | Default | Description |
|---|---|---|
| onSelect() => void | required | Runs on click, Enter and Space. |
| closeOnSelectboolean | true | Set false for a row that leaves the menu open — a filter toggle, say. |
| iconReactNode | required | Leading element. An icon at `size-4` reads best. |
| shortcutReactNode | required | Trailing hint. A plain string is rendered in the shortcut style. |
| descriptionReactNode | required | Second line under the label, for rows that need explaining. |
| variant"default" | "destructive" | "default" | `destructive` recolours the row and its icon. |
| disabledboolean | false | Skipped by the keyboard and by the pointer. |
| insetboolean | false | Indents past the indicator column, to line up with checkable rows. |
DropdownCheckboxItem
| Prop | Default | Description |
|---|---|---|
| checkedboolean | false | Whether the row shows its check. |
| onCheckedChange(checked: boolean) => void | required | Fires with the value the row is moving to. |
| closeOnSelectboolean | false | Defaults to staying open — checkable rows are usually toggled in runs. |
| iconReactNode | required | Leading element, after the check column. |
| shortcutReactNode | required | Trailing hint. |
| descriptionReactNode | required | Second line under the label. |
| variant"default" | "destructive" | "default" | `destructive` recolours the row and its icon. |
| disabledboolean | false | Skipped by the keyboard and by the pointer. |
DropdownRadioItem
| Prop | Default | Description |
|---|---|---|
| valuestring | required | Required. Reported to the enclosing `DropdownRadioGroup`. |
| closeOnSelectboolean | true | Defaults to closing: picking one of a set is a decision, not a toggle. |
| indicator"dot" | "check" | "dot" | The mark on the chosen row. A dot is the menu-radio convention; a check reads better for a list of places you switch between — a workspace, an account, a branch. |
| iconReactNode | required | Leading element, after the radio column. |
| shortcutReactNode | required | Trailing hint. |
| descriptionReactNode | required | Second line under the label. |
| variant"default" | "destructive" | "default" | `destructive` recolours the row and its icon. |
| disabledboolean | false | Skipped by the keyboard and by the pointer. |
DropdownSubContent
| Prop | Default | Description |
|---|---|---|
| side"top" | "bottom" | "left" | "right" | "right" | Preferred side. Flipped to the other one when there is no room. |
| align"start" | "center" | "end" | "start" | Which edge of the submenu lines up with its row. |
| sideOffsetnumber | 2 | Gap between the row and the submenu, in px. |
| alignOffsetnumber | -5 | Pulls the submenu back across the panel's padding and border, so its first row sits on the same line as the row that opened it. |