Dropdown, select and command menu

Dropdown menu, Combobox and Command menu show a list of actions or options in a floating panel. Use Dropdown menu for actions on the current item or view, Combobox to choose one value with search, and Command menu for app-wide ⌘K search.

  • Beta
  • Accessibility
  • Figma Dropdown Items 57:16405 · Dropdown 57:16368 · Command Menu 82:6468, 82:5901, 100:4781
  • packages/ui/src/components/{DropdownMenu,Combobox,CommandMenu,Menu,Kbd}

Beta until design supplies 3:1 boundaries for the toggle off track and the empty checkbox frame (C20, FC-06), confirms the 4px container gap (C16) and designs an item focus state (C10). Item sizes, selected styling, text colours and nested controls are resolved (C17–C19, C21).

Preview

All three components share one item anatomy, taken from Figma “Dropdown Items”. Configure an item, then open the menu to try it. The code updates with the controls.

 

tsx
import { DropdownMenu, DropdownMenuContent, DropdownMenuLabel, DropdownMenuTrigger, DropdownMenuItem, Button } from '@nuton/ui'

<DropdownMenu>
  <DropdownMenuTrigger asChild>
    <Button size="sm" appearance="stroke" trailingIcon={<RiArrowDownSLine />}>Lesson actions</Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent>
    <DropdownMenuLabel>Photosynthesis</DropdownMenuLabel>
    <DropdownMenuItem
      leading={<RiShareLine />}
      onSelect={copyLink}
    >
      Copy share link
    </DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>

The shortcut is display text only. Bind the key yourself; the Item options menu below shows a working one.

Installation

Availability

@nuton/ui and @nuton/tokens are private workspace packages. They aren't published to a registry yet (D4: registry deferred; D12: distribution waits on the provenance review). Inside the Nuton monorepo, add them as workspace dependencies. The commands below apply once the packages are published.

1. Add the packages

bash
pnpm add @nuton/ui @nuton/tokens @remixicon/react

Peer dependencies: react and react-dom 18 or later. @nuton/ui brings its own primitives: Radix Dropdown Menu, Popover and Dialog (D5) and cmdk. No provider is needed.

2. Load tokens and component styles once

tsx
// app/layout.tsx (or your app's entry file)
import '@nuton/tokens/tokens.css' // --nu-* design tokens
import '@nuton/ui/styles.css'     // component styles (uses the tokens)

Menus, the Combobox list and the Command menu render in a portal at the end of document.body, so a parent with overflow: hidden doesn't clip them. They stack with --nu-z-popover and --nu-z-modal.

Usage

Dropdown menu

tsx
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@nuton/ui'
import { RiEditLine, RiMore2Line } from '@remixicon/react'

export function LessonMenu({ title, onRename }: { title: string; onRename: () => void }) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button size="sm" appearance="ghost" iconOnly aria-label={`Options for ${title}`} leadingIcon={<RiMore2Line />} />
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem leading={<RiEditLine />} onSelect={onRename}>Rename</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Combobox

tsx
import { Combobox, Field } from '@nuton/ui'

<Field label="Reviewer">
  <Combobox options={students} value={reviewer} onValueChange={setReviewer} placeholder="Select a student" searchPlaceholder="Search…" />
</Field>

Command menu

tsx
import { CommandMenu, CommandMenuEmpty, CommandMenuGroup, CommandMenuItem } from '@nuton/ui'

<CommandMenu open={open} onOpenChange={setOpen} label="Search Nuton" placeholder="Search lessons, spaces and commands…">
  <CommandMenuEmpty>No results</CommandMenuEmpty>
  <CommandMenuGroup heading="Lessons">
    <CommandMenuItem onSelect={() => openLesson('photosynthesis')}>Photosynthesis</CommandMenuItem>
  </CommandMenuGroup>
</CommandMenu>
  • Use Dropdown menu for actions and view options; use Combobox when the result is a value in a form.
  • Open a dropdown from a Button with DropdownMenuTrigger asChild. An icon-only trigger needs an aria-label that names its object: “Options for Photosynthesis”.
  • Put Combobox inside a Field. The trigger is named by the field label and its current value.
  • Every visible and accessible string is passed in: placeholders, emptyText, searchLabel and footer hints. Pass translated strings.
  • Item labels are sentence case and start with a verb when they act: “Copy share link”, “Move to space”.

Variants and sizes

Figma's item types are not separate components: the leading visual (icon, flag, avatar or logo) goes in leading, and the row size comes from size.

Item types

Every Figma item type in both sizes, as real menus. Choose an option to select it; “Unavailable” is disabled. At Large, the leading visual sits in a 40px circular frame and a description shows on a second line.

Item options

One real menu with every Figma item option: checkboxes (with a tri-state “All topics”), toggles, sublabel, badge, description, a working shortcut (⌥N toggles notifications even when the menu is closed), a submenu and disabled items.

 

Gap: the Figma “Button” option (C21)

Figma allows a link or button inside an item. Interactive controls nested inside a menu item break keyboard and screen-reader behaviour, so the API doesn't support it: trailing is decorative and hidden from assistive technology, and development builds warn if it contains a button, a link or an onClick. Use a separate item, or a submenu for grouped actions. Figma correction to review: FC-07.

Sizes

ItemFigma labelHeightPaddingGapRadiusLeading
Dropdown item, sm364088820
Dropdown item, lg5656 (matches)8121040 frame
Command item, sm484410 / 12121020
Command item, md6464 (matches)12121040 frame

Code uses the geometry measured in Figma. Where a Figma variant label disagrees with its own frame, the measured value wins and the label is flagged for correction in Figma (C17; FC-02, FC-03). Figma itself has not been changed. Combobox options use the dropdown item sizes through optionSize.

ContainerValues
Dropdown menu and Combobox listMin width 280 (Combobox: the trigger's width), padding 8 including the 1px Stroke/Soft-200 border, gap 4, radius 16, regular-shadow/medium. Opens 10px from the trigger; submenus 8px.
ComboboxTrigger is a Text box: lg 48, sm 36. List scrolls after 320.
Command menuWidth 600 (narrower on small screens), search row 52, footer 52, radius 16. Opens 15% from the top; at most 70% of the viewport tall.
KbdPadding 2 / 6 including the 1px border, radius 4. Muted: Weak-50, at least 20 × 20.

Container gap: the DS documentation screenshot says 8px; all three composed menus on product screens use 4px, which code follows. Awaiting confirmation (C16).

Combobox trigger and option sizes

Keyboard shortcut chip

⌘Jesc

States and interactions

Menu items have no state props: Radix and cmdk set the state while people use the menu. Open each menu to see it. There are no component tokens to show these states statically.

Item states

Default and highlightedTransparent at rest. Pointing at an item or moving to it with the arrow keys fills it with Bg/Weak-50 (Figma Hover). Interact to see.
SelectedThe chosen option keeps Bg/Soft-200 and a check mark. Point at it: a 1px inset Stroke/Sub-300 ring marks hover instead of lightening it (C18).
Checked and toggledCheckbox and toggle items. The toggle keeps the menu open so several settings can be changed in a row.
Disabled“Archive (owner only)”: Text/Disabled-500, dimmed icons. Skipped by the arrow keys and can't be chosen.
No designed style
FocusFigma has no focus state for items. The highlight shows the active item and the browser's focus ring is kept (C10).

Combobox states

PlaceholderPlaceholder text uses Text/Sub-600 (C1). Open it: the border turns Stroke/Strong-950 and the chevron flips.
SelectedThe trigger shows the option's leading visual and label; in the list the option has the C18 selected style.

Choose a reviewer.

ErrorSet by Field error or invalid. The trigger uses the Text box error style and the message is linked.

Only owners can change this.

DisabledNative disabled button: removed from the tab order. Say why in the hint.

State tokens

StateHookStyle
Highlighted[data-highlighted] (Radix), [data-selected="true"] (cmdk)Bg/Weak-50
Selected.nu-menu-item--radio[data-state="checked"], [data-checked] (Combobox)Bg/Soft-200 + check mark Icon/Strong-950
Selected and highlightedBoth of the aboveBg/Soft-200 + 1px inset Stroke/Sub-300 ring (C18)
Disabled[data-disabled]Text/Disabled-500; icons and secondary text Icon/Disabled-300 at 60% opacity
Checkbox.nu-checkbox-indicator[data-state]Checked and indeterminate: State/Primary/Base. Unchecked: Bg/Soft-200 frame (C20)
Toggle.nu-switch-indicator[data-state]On: --nu-switch-on (State/Primary/Base); highlighted --nu-switch-on-hover (blue/700). Off: Bg/Soft-200; highlighted Bg/Sub-300

Composition examples

Lesson list with overflow menus

The View menu filters and sorts the list; each lesson's menu renames, duplicates, shares, moves, completes or deletes it. After Rename or Delete, focus moves to the lesson's menu button or the next one.

Lessons
  • Cell structureBiology 101 · In progress
  • Colour theoryDesign Principles · In progress
  • PhotosynthesisBiology 101 · Completed

 

Invite to a space

Combobox in forms: a search field, group headings (“Admins”), a small trigger with large options, disabled, and a required choice validated when the list closes.

Invite someone to this space.

Only owners can change this.

Search lessons, spaces and people

Press ⌘J / Ctrl J, or use the button. On this site ⌘K opens the documentation search, which is also a CommandMenu, so the demo uses ⌘J. CommandMenu doesn't bind a key itself; shortcut only shows it.

 

The footer link sits outside the item list, so it doesn't break C21. Items themselves never contain links.

API reference

Parts

Dropdown menu wraps Radix Dropdown Menu; Combobox and Command menu are built on cmdkinside a Radix Popover and Dialog. Parts not listed in the props tables below take their primitive's props unchanged.

PartPrimitiveNuton changes
DropdownMenu, DropdownMenuTrigger, DropdownMenuGroup, DropdownMenuRadioGroup, DropdownMenuSubRadix Root, Trigger, Group, RadioGroup, SubRe-exported unchanged
DropdownMenuContentRadix Content in a PortalDefaults sideOffset=10, align="start", collisionPadding=8
DropdownMenuSubContentRadix SubContent in a PortalDefaults sideOffset=8, collisionPadding=8
DropdownMenuItemRadix ItemItem props below
DropdownMenuCheckboxItemRadix CheckboxItemItem props; checked (boolean | 'indeterminate', default false) draws a checkbox on the left
DropdownMenuSwitchItemRadix CheckboxItemItem props; checked (boolean, default false) draws a toggle on the right; selecting never closes the menu
DropdownMenuRadioItemRadix RadioItemItem props; value required; check mark when selected (C18)
DropdownMenuSubTriggerRadix SubTriggerItem props without trailing; always shows a right chevron
DropdownMenuLabel, DropdownMenuSeparatorRadix Label, SeparatorStyles only (Figma group heading and “Content Divider”)
CommandMenuGroup, CommandMenuEmpty, CommandMenuSeparatorcmdk Group, Empty, SeparatorStyles only
MenuItemContent, CheckboxIndicator, SwitchIndicatorNoneThe shared item anatomy and the decorative indicators, exported for custom menus. Indicators are aria-hidden.

Item props (Dropdown menu items)

Dropdown menu item props, shared by DropdownMenuItem, CheckboxItem, SwitchItem, RadioItem and SubTrigger
PropTypeDefaultDescription
childrenrequiredReactNode—The label. Truncates with an ellipsis.
size'sm' | 'lg''sm'Row height 40 / 56px. lg frames the leading visual at 40px.
tone'default' | 'destructive''default'destructive sets the label and icon in the error colour, for Delete and Logout (Figma Dropdown Items).
leadingReactNode—Icon, flag, avatar or logo before the label (Figma Type). Rendered aria-hidden.
sublabelReactNode—Secondary text after the label, e.g. “(@sophia)”. Text/Sub-600 (C19).
descriptionReactNode—Second line under the label. Use with size="lg".
badgeReactNode—A badge element after the content, e.g. “New”.
shortcutReactNode—Shortcut hint; a string renders in a Kbd. Display only: bind the key yourself.
trailingReactNode—Decorative trailing visual, rendered aria-hidden. Never a button or link (C21; development warning). Not on SubTrigger.
classNamestring—Added to .nu-menu-item.
refRef<HTMLDivElement>—Forwarded to the Radix item.
…restRadix item props—Everything else from the matching Radix part: onSelect, disabled, textValue; checked / onCheckedChange on checkbox and switch items; value on radio items.

Combobox props

Combobox props
PropTypeDefaultDescription
optionsrequiredComboboxOption[]—The list. See ComboboxOption below.
valuestring | null—Controlled value. Omit for uncontrolled.
defaultValuestring | nullnullInitial value when uncontrolled.
onValueChange(value: string) => void—Called when an option is chosen. The list then closes.
onOpenChange(open: boolean) => void—Called when the list opens or closes, e.g. to validate a required choice.
placeholderstring—Trigger text when nothing is selected.
searchablebooleantrueShows the search field. Set false for short lists.
searchPlaceholderstring—Placeholder of the search field.
searchLabelstring'Search options'Accessible name of the search field. Pass a translated string.
emptyTextReactNode'No results'Shown when the search matches nothing. Pass a translated string.
size'sm' | 'lg''lg'Trigger height 36 / 48px (Text box sizes).
optionSize'sm' | 'lg''sm'Option row height 40 / 56px.
invalidboolean—Error style. Inherited from Field error when omitted.
disabledboolean—Disables the trigger. Inherited from Field when omitted.
requiredboolean—Sets required on the hidden input (needs name). Inherited from Field.
idstring—Trigger id. Defaults to the Field control id.
namestring—Renders a hidden input with the value, so the choice submits with a form.
classNamestring—Added to the trigger.
aria-labelstring—Names the trigger and list when there is no Field label.
aria-describedbystring—Extra description ids, merged with the Field hint and error.
refRef<HTMLButtonElement>—Forwarded to the trigger button.
ComboboxOption fields
PropTypeDefaultDescription
valuerequiredstring—Unique value passed to onValueChange.
labelrequiredstring—Visible label; also searched and shown in the trigger once selected.
sublabelReactNode—Secondary text, e.g. “@sophia”.
descriptionReactNode—Second line; use with optionSize="lg".
leadingReactNode—Icon, flag or avatar. Also shown in the trigger when selected.
badgeReactNode—A badge element.
disabledboolean—Option can’t be chosen.
groupstring—Group heading, e.g. “Admins”. Groups appear in order of first use.
keywordsstring[]—Extra search terms, such as a handle.

Command menu props

CommandMenu props
PropTypeDefaultDescription
labelrequiredstring—Accessible name of the dialog and the search field, e.g. “Search Nuton”.
childrenrequiredReactNode—Groups, items and CommandMenuEmpty.
openboolean—Controlled open state.
defaultOpenboolean—Initial open state when uncontrolled.
onOpenChange(open: boolean) => void—Called when Escape, an outside click or your code opens or closes it.
placeholderstring—Search field placeholder.
shortcutReactNode—Shortcut chip in the search row; a string renders in a Kbd. Display only.
footerReactNode—Footer content, usually CommandMenuFooter.
searchstring—Controlled search text.
onSearchChange(search: string) => void—Called as the search text changes.
shouldFilterboolean—Set false when your app filters the results, e.g. a server search.
containerHTMLElement | null—Portal container. Defaults to document.body. Pass an element to keep the palette inside a frame or preview.
modalbooleantrueSet false for a palette shown inside a preview that must not lock the page. It then draws no overlay, so the host draws its own.
classNamestring—Added to the dialog (.nu-command).
CommandMenuItem props
PropTypeDefaultDescription
childrenrequiredReactNode—The label.
size'sm' | 'md''sm'Row height 44 / 64px (Figma Small / Medium).
leadingReactNode—Icon, avatar, logo or flag. Rendered aria-hidden.
sublabelReactNode—Secondary text after the label.
descriptionReactNode—Second line; use with size="md".
badgeReactNode—A badge element.
shortcutReactNode—Shortcut hint; strings render in a Kbd. Display only.
trailingReactNode—Decorative, aria-hidden. Never a button or link (C21).
classNamestring—Added to .nu-menu-item.nu-command__item.
refRef<HTMLDivElement>—Forwarded to the cmdk item.
…restcmdk Item props—From cmdk: onSelect, value, keywords, disabled, forceMount.
CommandMenuFooter props
PropTypeDefaultDescription
hints{ keys: ReactNode[]; label: ReactNode }[][]Keyboard hints; each key renders in a muted Kbd. Labels are yours to translate.
childrenReactNode—Content on the end side, e.g. a Help Center link.

Kbd props

Kbd props
PropTypeDefaultDescription
variant'default' | 'muted''default'default: white chip for item and search-row shortcuts. muted: Weak-50 chip for footer hints.
refRef<HTMLElement>—Forwarded to the native kbd.
…restHTMLAttributes<HTMLElement>—Any native attribute (className, aria-*…).

Styling hooks

Items render .nu-menu-item with data-size and the state attributes listed under State tokens; containers render .nu-menu-content, .nu-combobox__content or .nu-command. Use props rather than styling these. The supported override points are the component tokens --nu-menu-min-width, --nu-menu-item-min-height, --nu-menu-list-max, --nu-command-width, --nu-command-footer-height, --nu-switch-on and --nu-switch-on-hover.

Accessibility

Keyboard

KeyBehaviour
Enter / Space / ↓On a dropdown trigger: opens the menu and focuses the first item.
Enter / SpaceOn a Combobox trigger: opens the list with focus in the search field.
↑ / ↓Moves between items, skipping disabled ones. Combobox and Command menu wrap at the ends.
Home / EndFirst or last item.
→ / ←Opens or closes a submenu.
EnterChooses the highlighted item. In a dropdown, Space does too.
A–ZDropdown: jumps to the item starting with that letter. Combobox and Command menu: filters the list.
EscCloses and returns focus to the trigger, or to whatever opened the Command menu.

Screen readers

Dropdown menus are WAI-ARIA menus (Radix): checkbox and toggle items are menuitemcheckbox, options are menuitemradiowith aria-checked. The check mark, checkbox and toggle visuals are aria-hidden, so state isn't announced twice. The Combobox trigger has aria-haspopup="listbox" and is named by the field label and its value; the search field controls a listbox (cmdk). The Command menu is a modal dialog named by label. Leading visuals are hidden, so the label must carry the meaning.

Focus

Focus moves into the menu on open and back to the trigger on close. In Combobox and Command menu it starts in the search field; the Command menu traps it and returns it to the opener. Figma has no focus state for items: the highlight marks the active item and the browser's focus ring is kept (C10). In forced-colours mode the highlighted item gets a Highlight outline.

Contrast and colour

Sublabels, descriptions and shortcuts use Text/Sub-600 instead of Figma's remote text/soft-400 (2.49:1): 6.69:1 on White, 6.24:1 on Weak-50, 5.61:1 on Soft-200 (C19). The selected check mark is 15.04:1, so selection doesn't rely on colour (C18). Toggle on is 5.59:1 on White. The toggle off track is State/Faded/Base, 4.23:1, and darker Icon/Sub-600 when highlighted, the same as Switch (C54; Figma's Soft-200 was 1.19:1). The empty checkbox frame now uses State/Faded/Base, 4.23:1 (C48).

Touch and pointer

Items are at least 40px tall (Command menu 44px) and fill the menu width, well above 24 × 24 (WCAG 2.5.8). Combobox triggers are 36 or 48px. Menus open on click or tap, never on hover; pointing at an item only highlights it.

You must: name icon-only triggers (“Options for Photosynthesis”); give CommandMenu a label; put Combobox in a Field or pass aria-label; never put buttons or links in an item (C21); bind any shortcut you show; pass translated strings for placeholders, emptyText, searchLabel and footer hints.

Figma mapping

FigmaVariantsCode
Dropdown Items (57:16405)Type: Basic · Country · Avatar · Provider · Brand · Company — State: Default · Hover · Selected · Selected Hover · Disabled — Size: Small · Large (60 variants in total)leading (any icon/flag/avatar/logo), size="sm" | "lg"; Hover = highlighted, Selected = radio/checked option, Disabled = disabled
Item optionsCheckbox · Sublabel · Description · Badge · Shortcut · Toggle · Button · Right iconDropdownMenuCheckboxItem, sublabel, description, badge, shortcut, DropdownMenuSwitchItem, trailing (decorative; no Button, C21), DropdownMenuSubTrigger
Composition (57:16368)Container, dividers, 10px from trigger, searchDropdownMenuContent, DropdownMenuSeparator, DropdownMenuLabel, Combobox
Command Menu Search Input (82:6468)Default · Hover · Active; Shortcut; ButtonCommandMenu placeholder, shortcut
Command Menu Items (82:5901)Type: Basic · Avatar · Left Icon · Brand · Company · Country — Default · Hover — Small · MediumCommandMenuItem leading, size="sm" | "md", sublabel, description, badge, shortcut
Command Menu Footer (100:4781)Key hints + help linkCommandMenuFooter hints, children

Deviations from Figma

IDKindWhat code does
C16Open questionContainer gap 4 (product screens) rather than 8 (DS documentation screenshot). Awaiting confirmation.
C17Figma discrepancyMeasured heights: Dropdown 40 / 56, Command 44 / 64. Labels to fix in Figma (FC-02, FC-03).
C18Product decision (deviates from Figma)Figma's Selected Hover lightens the item to Weak-50, which makes it look unselected. In code the selected item keeps Soft-200 and a check mark (15.04:1, so selection doesn't rely on colour), and hover adds a 1px inset Stroke/Sub-300 ring. The ring is decorative only (FC-04).
C19Product decision (D7)Sublabels, descriptions and shortcuts use local Nuton Text/Sub-600 instead of Figma's external text/soft-400 (2.49:1). Measured: 6.69:1 on White, 6.24:1 on Weak-50, 5.61:1 on Soft-200. All pass AA (FC-05).
C20Brand override (deviates from Figma)Toggle "on" uses Nuton State/Primary/Base #3559E9 instead of the external library's #335CFF. Still failing and flagged for design: the "off" track and the empty checkbox frame (Soft-200) are 1.19:1 against White, below 3:1 (FC-06). The standalone Switch component is tracked separately (CI-01).
C21Product decision (deviates from Figma)No buttons or links inside a menu item. trailing is decorative and hidden from assistive technology. Development builds warn if it contains a button, a link or an onClick (FC-07).

Guidelines and related components

Use these for

  • Dropdown menu: secondary actions on a lesson or space, and view options such as sort and filters.
  • Combobox: choosing one value from a long or rich list in a form, such as a student or a country.
  • Command menu: jumping to any lesson, space or person, and running commands from anywhere.

Don't use them for

  • One or two actions that fit on screen. Use Button.
  • Choosing between 2–5 options people should see at once. Use Button group.
  • A short, plain list in a form. Use the native Select on Inputs and fields.
  • Several values at once in a form. Combobox chooses one value.

Do and don't

DoUse Combobox when the result is a value. It shows the choice, works with Field and submits with the form.
Don'tDon't use a dropdown menu as a select. A menu of actions doesn't expose a value to forms or screen readers.
DoSay what the trigger controls. Showing the current value saves people opening the menu to check.
Don'tDon't use a vague trigger label. “Options” gives no hint of what's inside.
DoCollect row actions in one overflow menu. One named button per lesson keeps the list calm and the tab order short.
Don'tDon't line up many icon-only buttons per row. Each one is a tab stop and a guess.
DoShow a shortcut only when it's bound and free. This site's demo uses ⌘J because ⌘K is taken by docs search.
Don'tDon't advertise a key that does something else. The hint promises a shortcut the page can't keep.

Common mistakes

  • Putting a button or link in trailing (C21). Make it a separate item instead.
  • Showing shortcut without binding the key. The prop only draws the hint.
  • Expecting a checkbox list to stay open. Checkbox items close the menu like any item; add onSelect={(e) => e.preventDefault()}. Toggle items stay open on their own.
  • Relying on the leading icon or avatar for meaning. It's hidden from screen readers; the label must say it all.
  • Forgetting name on a Combobox in a plain HTML form, so the value isn't submitted.