Dropdown

Menu button for actions - grouped rows, shortcuts, and submenus that nest as deep as you like.

Dropdown playground

Submenus always align to their own row - side and align place the top-level menu only.

Install Dropdown

1. New project? One command installs the package and wires everything - see Installation:

bash
pnpm dlx zyncat-ui init

2. Import the component - it loads its own stylesheet:

tsx
import { Dropdown } from '@zyncat/ui/dropdown';

Usage

tsx
import { Dropdown } from '@zyncat/ui/dropdown';
 
<Dropdown ariaLabel="Post options" trigger={<Button>Options</Button>} items={items} />

Compose Dropdown in your application. No Tailwind or external styling library is required; every value resolves from Zyncat UI's token vocabulary.

Dropdown props

Dropdown

PropTypeDefaultDescription
items*DropdownItems[]The rows - a flat `DropdownItem[]`, or `DropdownGroup[]` to render divided sections. A row with its own `items` opens a submenu instead of committing.
trigger*ReactElement—Cloned to toggle the menu, and used as the anchor. Gets the `aria-haspopup="menu"` wiring.
activateOnActivateOn'pointerdown'Whether the trigger and the rows fire on `pointerdown` (snappier) or wait for `click`.
openboolean—Controlled open state. Omit to stay uncontrolled.
defaultOpenbooleanfalseInitial state when uncontrolled.
onOpenChange(open: boolean) => void—Fires whenever the open state changes. Pair with `open` for controlled use.
onSelect(id: string, item: DropdownItem) => void—Fires when a row commits - gets its `id` and the full item. Committing closes every level.
returnFocusbooleantrueMove focus back to the trigger when a row commits or the keyboard dismisses the menu. Turn off when rows place focus themselves - an editor command that refocuses its document.
side'top' | 'bottom' | 'left' | 'right''bottom'Preferred side of the trigger; flips to the opposite side when cramped.
align'start' | 'center' | 'end''start'Cross-axis alignment against the trigger. Submenus always align to their row.
highlight'neutral' | 'accent''neutral'Hue of the highlight that travels between rows: the neutral wash, or the accent wash with accent ink on the active row.
sizeMenuSize'md'Menu density - row type and padding on every row, submenus included; a row carrying a description sits one step taller than a plain one. The trigger is your own node, so this never touches it.
weightMenuWeight'medium'Weight of every row label.
widthMenuWidth'auto'Width of the top-level menu. `auto` fits the rows, `trigger` is never narrower than the trigger, and `sm` | `md` | `lg` are fixed steps with long labels ellipsizing. Submenus always fit their rows.
railbooleanfalseShort accent bar on the leading edge of the highlight, marking the active row.
idstring—Base id for the menu and its rows; drives the trigger's `aria-controls`. Auto-generated when omitted.
ariaLabelstring—Accessible name for the menu - supply when the trigger's own label does not describe it.
...htmlAttributesHTMLAttributes<HTMLDivElement> & DataAttributes—Standard attributes (className, style, data-*, ...) forwarded to the top-level menu panel.
animationDisableableAnimationopen 'base'/'entrance', close 'fast'/'exit'Open/close timing - motion tokens only, or `null` to disable.

DropdownGroup

PropTypeDefaultDescription
labelstring—Section heading rendered above the rows; omit for an unlabeled but still divided group.
items*DropdownItem[]—The rows in this section.

DropdownItem

PropTypeDefaultDescription
id*string—Unique id - what `onSelect` receives, and what identifies this row's open submenu.
label*ReactNode—Primary row text.
descriptionstring—Optional secondary line under the label.
iconReactNode—Your own icon node shown before the label.
shortcutstring—Keyboard hint shown at the trailing edge, mono. Display only - you still bind the key.
dangerbooleanfalseDestructive action - the row reads in the danger hue.
disabledbooleanfalseNot selectable - skipped by arrow keys and typeahead, and marked `aria-disabled`.
selectedboolean—Marks the row as the current value of a single-choice group: accent ink with a check at rest, and `menuitemradio` semantics. Leave undefined for plain action rows.
itemsDropdownItem[] | DropdownGroup[]—Nested menu. The row opens it instead of committing, and can nest again without limit.
contentReactNode—Your own panel body, opened from this row in place of a submenu. Nothing inside it commits the menu - drive dismissal with `open`/`onOpenChange`.
searchTextstring—Text used for typeahead. Required when `label` is not a string.
onSelect() => void—Fires when this row commits, before the menu's own `onSelect`.
Set in Geist & Newsreader — animated by the house engineZyncat UI · Rev 0.11 · MIT · Built by Tabsir Ahammed · Source on GitHub