Dropdown menu
A focused list of actions, right where you need it.
import { DropdownMenu } from '@uthereal/ui/dropdown-menu'Overview
An independent dropdown with nested actions, checkboxes, keyboard navigation, and interruptible motion. Change its appearance and behavior through typed arguments, or let an agent supply those arguments at runtime.
Use it for a short list of commands behind a button: file actions, row actions, or view settings. For choosing a form value, use a select instead.
Installation
Uthereal UI is currently a private source package. From a checkout of this repository, create a package archive:
npm packInstall that archive in your React application:
npm install /path/to/uthereal-ui-0.1.0.tgzUse React 19.2.3 or later and a bundler that handles TypeScript and CSS Modules, such as Next.js or Vite. The package installs Zag, Motion, and Zod as dependencies. Component styles are imported with the component.
Usage
import { DropdownMenu } from '@uthereal/ui/dropdown-menu'
export function FileActions() {
return (
<DropdownMenu
label="Actions"
items={[
{ kind: 'action', id: 'rename', label: 'Rename', action: 'rename' },
{
kind: 'action',
id: 'duplicate',
label: 'Duplicate',
action: 'duplicate',
},
{ kind: 'separator', id: 'separator' },
{
kind: 'checkbox',
id: 'preview',
label: 'Show preview',
defaultChecked: true,
},
]}
onAction={(action, itemId) => console.log(action, itemId)}
/>
)
}The component emits named actions. Your application implements what those actions do.
Examples
Nested actions
Submenus with keyboard and pointer navigation.
Persistent choices
Checkboxes keep the menu open as you select.
Select an item to see its event.
Nested actions
A submenu is an item with its own items array. Item IDs must be unique across the whole menu.
const items = [
{
kind: 'submenu',
id: 'export',
label: 'Export',
items: [
{ kind: 'action', id: 'pdf', label: 'PDF document', action: 'exportPdf' },
{
kind: 'action',
id: 'csv',
label: 'CSV spreadsheet',
action: 'exportCsv',
},
],
},
] satisfies import('@uthereal/ui').MenuItem[]Controlled state
Keep checkbox selections in your application when they must survive a remount. Omit these props to let the component manage its own state.
const [checked, setChecked] = useState({ preview: true })
<DropdownMenu
items={items}
checked={checked}
onCheckedChange={setChecked}
/>API reference
These serializable arguments and defaults come directly from the component schema.
| Argument | Type / default | Description |
|---|---|---|
label | stringDefault: "Actions" | Visible trigger text and accessible menu name. |
itemsRequired | MenuItem[]— | Actions, checkboxes, separators, and nested submenus. Each item needs a stable, unique ID. |
placement | bottom-start | bottom | bottom-end | top-start | top | top-end | left-start | left | left-end | right-start | right | right-endDefault: "bottom-end" | Preferred popup position. Collision detection can adjust it. |
size | sm | md | lgDefault: "md" | Trigger and menu item density. |
variant | solid | outline | ghostDefault: "outline" | Trigger appearance. |
recipe | studio | softDefault: "studio" | Visual treatment. Both recipes use the same interaction behavior. |
motion | none | subtle | expressiveDefault: "subtle" | Animation preset. System reduced-motion preferences take precedence. |
motionOptions | MotionOptions— | Popup, highlight, label, and chevron overrides. Durations are milliseconds. See Motion. |
theme | DropdownTheme— | Color mode, accent, and semantic tokens. See Theming for all keys and defaults. |
dir | ltr | rtlDefault: "ltr" | Reading direction and submenu navigation. |
closeOnSelect | booleanDefault: true | Close after selecting an action. Checkbox items stay open unless overridden. |
loopFocus | booleanDefault: false | Wrap keyboard navigation at the first and last items. |
disabled | booleanDefault: false | Disable the trigger. |
defaultOpen | booleanDefault: false | Initial open state. Later argument edits do not reset an existing instance. |
React integration
| Prop | Purpose |
|---|---|
onAction(action, itemId) | Handle named actions in your application. |
open / onOpenChange(open) | Control whether the menu is open. |
checked / onCheckedChange(checked, changedId) | Control checkbox selections by item ID. |
tokens | CSS variables applied to both trigger and portalled menus. |
preset | Application defaults, overridden by explicit instance arguments. |
renderTrigger(props, state) | Render a custom button; forward all supplied props. |
triggerRef | Access the native trigger button. |
className | Class applied to the component root. |
portalContainer | Place the popup inside a native dialog's top layer. |
Keyboard & accessibility
| Key | Interaction |
|---|---|
Enter, Space, ↓ | Open the menu from its trigger. |
↑ / ↓ | Move between enabled items. |
Home / End | Move to the first or last enabled item. |
→ / ← | Enter or leave a submenu; directions reverse in RTL. |
| Type a label | Jump to a matching item. |
Escape | Close the current menu and restore focus. |
Tab | Dismiss the menu and continue through the page. |
Zag supplies focus management, menu semantics, typeahead, and collision handling. Disabled items are skipped. Use clear labels for destructive actions; color alone does not communicate their effect.
Motion
subtle is the default. expressive adds a more pronounced spring; none removes animation. Pointer highlights animate between items, while keyboard highlights respond immediately. System reduced-motion preferences always take precedence.
Override individual settings while retaining the preset's remaining defaults:
<DropdownMenu
items={items}
motion="expressive"
motionOptions={{
transition: { type: 'spring', damping: 38 },
enter: { offset: 6 },
chevronDuration: 180,
}}
/>All durations are milliseconds. Explore the motion controls and complete reference.
Theming
Choose light or dark mode, an accent, and optional design tokens. The same typed settings apply to the trigger and every portalled submenu, and are available to agents.
<DropdownMenu
items={items}
theme={{
mode: 'dark',
accent: 'violet',
tokens: { popupRadius: 8, fontSize: 14, itemHeight: 36 },
}}
/>Recipe and size supply defaults for omitted tokens. Geometry uses pixels. Explore all 30 tokens in the theming reference, or open Customize in the preview above. Appearance edits preserve existing checkbox selections and open-menu state.