NimUI
ComponentsForms

Combobox

A searchable, filterable dropdown input built on cmdk and Radix Popover

The Combobox provides a searchable, filterable dropdown input — perfect for selecting from large datasets where a plain Select becomes unwieldy. It composes cmdk for keyboard navigation and filtering with Radix Popover for positioning, and inherits nim-ui's OKLCH tokens, focus rings, and dark-mode styling.

Import

import {
  Combobox,
  ComboboxTrigger,
  ComboboxContent,
  ComboboxInput,
  ComboboxList,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxItem,
  ComboboxSeparator,
} from '@nim-ui/components';

Basic Usage

Combobox is a controlled component — manage open and the selected value yourself so you can react to changes (submit a form, sync to URL state, etc.).

Framework Picker
Code
<Combobox open={open} onOpenChange={setOpen}>
  <ComboboxTrigger className="w-64">
    {selected?.label ?? 'Select a framework…'}
  </ComboboxTrigger>
  <ComboboxContent>
    <ComboboxInput placeholder="Search frameworks…" />
    <ComboboxList>
      <ComboboxEmpty>No framework found.</ComboboxEmpty>
      <ComboboxGroup heading="Frameworks">
        {frameworks.map((framework) => (
          <ComboboxItem
            key={framework.value}
            value={framework.value}
            onSelect={(current) => {
              setValue(current === value ? '' : current);
              setOpen(false);
            }}
          >
            {framework.label}
          </ComboboxItem>
        ))}
      </ComboboxGroup>
    </ComboboxList>
  </ComboboxContent>
</Combobox>

Full implementation, including the state and data used above:

import { useState } from 'react';
import {
  Combobox,
  ComboboxTrigger,
  ComboboxContent,
  ComboboxInput,
  ComboboxList,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxItem,
} from '@nim-ui/components';

const frameworks = [
  { value: 'next', label: 'Next.js' },
  { value: 'remix', label: 'Remix' },
  { value: 'astro', label: 'Astro' },
  { value: 'sveltekit', label: 'SvelteKit' },
  { value: 'nuxt', label: 'Nuxt' },
];

export function FrameworkPicker() {
  const [open, setOpen] = useState(false);
  const [value, setValue] = useState('');

  const selected = frameworks.find((f) => f.value === value);

  return (
    <Combobox open={open} onOpenChange={setOpen}>
      <ComboboxTrigger className="w-64">
        {selected?.label ?? 'Select a framework…'}
      </ComboboxTrigger>
      <ComboboxContent>
        <ComboboxInput placeholder="Search frameworks…" />
        <ComboboxList>
          <ComboboxEmpty>No framework found.</ComboboxEmpty>
          <ComboboxGroup heading="Frameworks">
            {frameworks.map((framework) => (
              <ComboboxItem
                key={framework.value}
                value={framework.value}
                onSelect={(current) => {
                  setValue(current === value ? '' : current);
                  setOpen(false);
                }}
              >
                {framework.label}
              </ComboboxItem>
            ))}
          </ComboboxGroup>
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  );
}

With Groups and Separator

Use ComboboxGroup with a heading prop to label sections. Insert ComboboxSeparator between groups for a visual divider.

<Combobox open={open} onOpenChange={setOpen}>
  <ComboboxTrigger>Select an action…</ComboboxTrigger>
  <ComboboxContent>
    <ComboboxInput placeholder="Type a command…" />
    <ComboboxList>
      <ComboboxEmpty>No results found.</ComboboxEmpty>

      <ComboboxGroup heading="Suggestions">
        <ComboboxItem value="calendar">Calendar</ComboboxItem>
        <ComboboxItem value="search">Search Emoji</ComboboxItem>
        <ComboboxItem value="calculator">Calculator</ComboboxItem>
      </ComboboxGroup>

      <ComboboxSeparator />

      <ComboboxGroup heading="Settings">
        <ComboboxItem value="profile">Profile</ComboboxItem>
        <ComboboxItem value="billing">Billing</ComboboxItem>
        <ComboboxItem value="settings">Settings</ComboboxItem>
      </ComboboxGroup>
    </ComboboxList>
  </ComboboxContent>
</Combobox>

Disabled Items

Mark individual items as disabled — they remain visible but can't be selected.

<ComboboxItem value="archived" disabled>
  Archived (coming soon)
</ComboboxItem>

Props

ComboboxContent

NameTypeDefaultDescription
width'trigger' | 'auto''trigger'Width strategy. "trigger" matches the trigger width via --radix-popover-trigger-width, "auto" fits content
sideOffsetnumber4Distance in pixels between the trigger and the content
classNamestring-Additional CSS classes for the content container

ComboboxItem

NameTypeDefaultDescription
value*string-Unique value used for filtering and selection
onSelect(value: string) => void-Callback fired when the item is selected (click or Enter key)
disabledbooleanfalsePrevent the item from being selected

Accessibility

  • Trigger has role="combobox" and supports aria-expanded via Radix Popover state
  • Full keyboard support through cmdk: Arrow Up/Down to navigate, Enter to select, Escape to close
  • Typing in ComboboxInput filters items with fuzzy matching
  • Active item tracked with aria-activedescendant, highlighted via data-[selected=true]
  • Focus automatically moves to the input when the popover opens
  • Disabled items skipped by keyboard navigation via data-[disabled=true]
  • Use ComboboxEmpty to announce empty states to screen readers

Keyboard Support

KeyAction
Arrow DownMove focus to next item
Arrow UpMove focus to previous item
EnterSelect the focused item
EscapeClose the popover
HomeMove to first item
EndMove to last item

On this page