oks-ui

How to use

Dropdown

Dropdown is a compound menu component: Dropdown owns the open state and positioning, DropdownTrigger wraps whatever element opens it, and DropdownMenu holds DropdownItems (or DropdownSections of them). It supports single/multiple selection, nested submenus, and opt-in virtualization for long lists, on top of full keyboard navigation and typeahead.

import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Button } from "oks-ui";

The compound shape

DropdownTrigger takes exactly one child — the element that opens the menu on press — and DropdownMenu holds the items. onAction fires with the activated item's key; that's usually all you need instead of an onClick per item.

<Dropdown>
  <DropdownTrigger>
    <Button variant="bordered">Actions</Button>
  </DropdownTrigger>
  <DropdownMenu aria-label="Actions" onAction={(key) => handleAction(String(key))}>
    <DropdownItem key="edit">Edit</DropdownItem>
    <DropdownItem key="duplicate">Duplicate</DropdownItem>
    <DropdownItem key="delete" className="text-danger">Delete</DropdownItem>
  </DropdownMenu>
</Dropdown>

Selection modes

selectionMode defaults to "none" (a plain action menu). Set it to "single" or "multiple" for a menu that tracks its own selected item(s) — pair with selectedKeys/onSelectionChange to control it, or defaultSelectedKeys to just seed the initial state and let Dropdown manage it from there.

<DropdownMenu
  aria-label="Sort by"
  selectionMode="single"
  selectedKeys={[sortKey]}
  onSelectionChange={(keys) => setSortKey([...keys][0] as string)}
>
  <DropdownItem key="name">Name</DropdownItem>
  <DropdownItem key="date">Date</DropdownItem>
</DropdownMenu>

Nested submenus

An item with its own DropdownMenu inside it becomes a submenu, opened on hover or ArrowRight — submenuPlacement controls which side it opens on when the default (right-start) would run out of viewport space.

<DropdownItem key="export">
  Export
  <DropdownMenu aria-label="Export format">
    <DropdownItem key="csv">CSV</DropdownItem>
    <DropdownItem key="json">JSON</DropdownItem>
  </DropdownMenu>
</DropdownItem>

Long lists: opt-in virtualization

For a menu with hundreds of items (a searchable list of every country, every user in a workspace), pass virtualization={{ itemHeight }} on DropdownMenu so only the visible slice of items actually renders to the DOM. Leave it off for ordinary menus — it's specifically for lists long enough that full rendering would be the bottleneck.

<DropdownMenu aria-label="Country" items={countries} virtualization={{ itemHeight: 36 }}>
  {(country) => <DropdownItem key={country.code}>{country.name}</DropdownItem>}
</DropdownMenu>

Positioning and dismissal

placement (12 directional options) plus shouldFlip (on by default, so the menu flips to the opposite side automatically when it would otherwise run off-screen) control layout; isDismissable/isKeyboardDismissDisabled/shouldCloseOnInteractOutside control how it can be closed, and closeOnSelect (on by default) decides whether picking an item closes the menu — turn it off for a multi-select menu you want to stay open across several picks.

Accessibility

  • Implements the full menu/menuitem ARIA pattern with roving tabindex, arrow-key navigation, and typeahead (typing jumps to the next matching item).
  • When an item's visible label is a non-text node (an icon plus a badge, say), pass textValue with the plain-text equivalent so typeahead and the accessible name both still work correctly.
  • Always give DropdownMenu its own aria-label (or aria-labelledby) — the trigger button's own label doesn't automatically describe what the opened menu contains.
  • isReadOnly items are visible and reachable by keyboard but not selectable — different from isDisabled, which removes them from keyboard navigation entirely.

Props reference

All available props for Dropdown. See the full component page for an interactive playground.

Props

children

ReactNode

Default:

A DropdownTrigger and a DropdownMenu.

Best practices

  • Keep selectionMode="none" for plain action menus (Edit/Duplicate/Delete) — only reach for "single"/"multiple" when the menu is genuinely representing a choice, not a set of one-shot actions.
  • Turn closeOnSelect off for multi-select menus where picking several items in one sitting is the expected flow; leave it on for single actions, where staying open after a pick just adds an extra dismiss step.
  • Reserve virtualization for lists long enough to actually need it (order of hundreds of items) — for a typical 5-15 item menu it's unnecessary overhead.
  • Use showDivider on an item to separate a destructive action (Delete) from the rest of the menu, rather than relying on color alone to signal it's different.
View full API reference for Dropdown