Combobox

A searchable dropdown that combines select functionality with powerful filtering. Perfect for finding options in large lists, building command palettes, or when users need quick access through search.

This component's API and documentation are under review and may change. Use with caution in production.

A searchable dropdown that combines select functionality with powerful filtering. Perfect for finding options in large lists, building command palettes, or when users need quick access through search.

Built on top of cmdk for search and filtering, and Radix Popover for positioning and focus management.

Playground

Installation

npm install @kushagradhawan/kookie-ui

Usage

The trigger shows displayValue for the selected value. Without it, the trigger shows the raw value (for example apple, not Apple).

import { Combobox } from '@kushagradhawan/kookie-ui';
 
const fruits = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
];
 
// Module scope keeps the function stable across renders
const getFruitLabel = (value: string | null) => fruits.find((fruit) => fruit.value === value)?.label;
 
export function FruitPicker() {
  return (
    <Combobox.Root displayValue={getFruitLabel}>
      <Combobox.Trigger aria-label="Fruit">
        <Combobox.Value placeholder="Select a fruit" />
      </Combobox.Trigger>
      <Combobox.Content>
        <Combobox.Input placeholder="Search fruits..." />
        <Combobox.List>
          {fruits.map((fruit) => (
            <Combobox.Item key={fruit.value} value={fruit.value}>
              {fruit.label}
            </Combobox.Item>
          ))}
          <Combobox.Empty>No fruits found</Combobox.Empty>
        </Combobox.List>
      </Combobox.Content>
    </Combobox.Root>
  );
}

Anatomy

Combobox is a compound component with the following parts:

<Combobox.Root>
  <Combobox.Trigger>
    <Combobox.Value />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input />
    <Combobox.List>
      <Combobox.Group>
        <Combobox.Label />
        <Combobox.Item />
      </Combobox.Group>
      <Combobox.Separator />
      <Combobox.Empty />
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Root Props

The Root component manages state and provides context to all child components.

PropTypeDescription
size'1' | '2' | '3'Combobox density: 1 (24px) for toolbars, 2 (32px) for standard forms, 3 (40px) for prominent selections. Supports responsive objects
valuestring | nullControlled selection value
defaultValuestring | nullInitial selection value for uncontrolled usage
onValueChange(value: string | null) => voidCallback fired when selection changes
openbooleanControlled open state
defaultOpenbooleanInitial open state
onOpenChange(open: boolean) => voidCallback fired when dropdown opens/closes
placeholderstringText shown when no option is selected (default: "Select an option")
searchPlaceholderstringPlaceholder for search input (default: "Search...")
searchValuestringControlled search input value
defaultSearchValuestringInitial search value
onSearchValueChange(value: string) => voidCallback fired when search changes
shouldFilterbooleanEnable built-in fuzzy search (default: true)
filter(value, search, keywords) => numberCustom filter function. Returns 0-1 where 0 is no match, 1 is exact match
loopbooleanAllow keyboard navigation to cycle through options (default: true)
disabledbooleanPrevents all interaction with the combobox
resetSearchOnSelectbooleanClear the search when an option is selected (default: true)
resetSearchOnClosebooleanClear the search when the popup is dismissed without a selection (default: true)
colorAccentColorSemantic color for the trigger and selected items
displayValuestring | ((value) => string | undefined)Text shown in the trigger for the selected value. Without it the raw value is shown. Pass a stable function (module scope or useCallback); an inline function runs on every render

Trigger Props

The Trigger component renders as a button that opens the dropdown.

PropTypeDescription
variant'classic' | 'surface' | 'soft' | 'outline' | 'ghost'Trigger button style: classic for elevated, surface for standard fields, outline for bordered, soft for subtle, ghost for minimal
colorAccentColorSemantic color. Overrides color from Root if provided
radius'none' | 'small' | 'medium' | 'large' | 'full'Corner radius scale
material'solid' | 'translucent'Trigger background: solid for opaque, translucent for depth over images
panelBackground'solid' | 'translucent'Deprecated: Use material instead
loadingbooleanShows a spinner, sets aria-busy, and keeps the popup from opening. The trigger stays focusable
errorbooleanVisual error state and aria-invalid, typically for validation failures
readOnlybooleanShows the value and sets aria-readonly. The popup does not open
disabledbooleanPrevents interaction. Overrides Root disabled if provided
widthstringTrigger width (CSS value)
placeholderstringOverride placeholder from Root
aria-labelstringAccessible name. The trigger needs one of aria-label, aria-labelledby, or a <label htmlFor>

Trigger also supports all margin props (m, mx, my, mt, mr, mb, ml) with responsive values.

Content Props

The Content component renders the dropdown surface containing the search input and options list.

PropTypeDescription
variant'solid' | 'soft'Content surface style: solid for opaque standard background, soft for subtle content-heavy interfaces
size'1' | '2' | '3'Override size from Root. Affects padding and item height
colorAccentColorSemantic color for items. Falls back to Root color, then theme accent
material'solid' | 'translucent'Surface background. Falls back to the Theme material
panelBackground'solid' | 'translucent'Deprecated: Use material instead
width, minWidth, maxWidthstringSurface width. minWidth defaults to the trigger width, maxWidth to 480px
aria-labelstringAccessible name for the popup, search field and list. Defaults to the trigger's name
containerHTMLElementPortal container. Defaults to document.body
forceMountbooleanKeep content mounted while closed. Last resort: closed content keeps its positioning and dismiss listeners running. Use displayValue on Root instead

Input Props

The Input component renders the search field inside Content.

PropTypeDescription
variant'classic' | 'surface' | 'soft' | 'outline' | 'ghost'Input field style. Defaults to 'surface' for solid Content, 'soft' for soft Content
placeholderstringSearch input placeholder. Overrides searchPlaceholder from Root
startAdornmentReactNodeIcon or element before input (e.g., search icon)
endAdornmentReactNodeIcon or element after input
valuestringOverride controlled search value from Root
onValueChange(value: string) => voidOverride search change handler from Root

Item Props

PropTypeDescription
valuestringValue passed to onValueChange. Without it, the item's text is used
labelstringText matched by search. Defaults to the text of the children. Set it when the children are components whose text is not written in the JSX
keywordsstring[]Extra search terms
disabledbooleanSkips the item in keyboard navigation and blocks selection
onSelect(value: string) => voidCalled when the item is selected

Search matches the item's visible text, its value, and its keywords.

Other Components

  • Combobox.Value - Shows displayValue, else the raw selected value, else the placeholder
  • Combobox.List - Scrollable listbox that holds the items. label overrides its accessible name
  • Combobox.Group - Groups related items together
  • Combobox.Label - Non-interactive heading. Inside a Group, it names the group for screen readers
  • Combobox.Separator - Visual separator between items or groups
  • Combobox.Empty - Shown when no results match search. Only visible when filtering produces zero matches

Trigger Variants

Classic

Elevated style with subtle shadow for maximum visual impact.

<Combobox.Root size="2">
  <Combobox.Trigger variant="classic" aria-label="Option">
    <Combobox.Value placeholder="Select option" />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="1">Option 1</Combobox.Item>
      <Combobox.Item value="2">Option 2</Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Surface

Elevated surface style for standard form fields.

<Combobox.Root size="2">
  <Combobox.Trigger variant="surface" aria-label="Option">
    <Combobox.Value placeholder="Select option" />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="1">Option 1</Combobox.Item>
      <Combobox.Item value="2">Option 2</Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Soft

Subtle background for secondary selections.

<Combobox.Root size="2">
  <Combobox.Trigger variant="soft" aria-label="Option">
    <Combobox.Value placeholder="Select option" />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="1">Option 1</Combobox.Item>
      <Combobox.Item value="2">Option 2</Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Outline

Bordered style for clear field boundaries.

<Combobox.Root size="2">
  <Combobox.Trigger variant="outline" aria-label="Option">
    <Combobox.Value placeholder="Select option" />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="1">Option 1</Combobox.Item>
      <Combobox.Item value="2">Option 2</Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Ghost

Minimal style for utility controls.

<Combobox.Root size="2">
  <Combobox.Trigger variant="ghost" aria-label="Option">
    <Combobox.Value placeholder="Select option" />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="1">Option 1</Combobox.Item>
      <Combobox.Item value="2">Option 2</Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Content Variants

Solid

Default opaque background for standard dropdowns with clear separation from the page.

<Combobox.Root size="2">
  <Combobox.Trigger variant="surface" aria-label="Option">
    <Combobox.Value placeholder="Solid content" />
  </Combobox.Trigger>
  <Combobox.Content variant="solid">
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="1">Option 1</Combobox.Item>
      <Combobox.Item value="2">Option 2</Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Soft

Subtle background for content-heavy interfaces where less visual weight is desired.

<Combobox.Root size="2">
  <Combobox.Trigger variant="surface" aria-label="Option">
    <Combobox.Value placeholder="Soft content" />
  </Combobox.Trigger>
  <Combobox.Content variant="soft">
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="1">Option 1</Combobox.Item>
      <Combobox.Item value="2">Option 2</Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Sizes

Set size on Root to control density across the entire combobox. Affects trigger height, content padding, and item spacing.

// Size 1 - 24px trigger, for toolbars and dense interfaces
<Combobox.Root size="1">
  <Combobox.Trigger variant="surface" aria-label="Option">
    <Combobox.Value placeholder="Size 1" />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="1">Option 1</Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>
 
// Size 2 - 32px trigger, standard for most forms
<Combobox.Root size="2">
  <Combobox.Trigger variant="surface" aria-label="Option">
    <Combobox.Value placeholder="Size 2" />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="1">Option 1</Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>
 
// Size 3 - 40px trigger, for prominent selections and touch targets
<Combobox.Root size="3">
  <Combobox.Trigger variant="surface" aria-label="Option">
    <Combobox.Value placeholder="Size 3" />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="1">Option 1</Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Colors

Use the color prop on Root to set semantic colors for the trigger and selected items. Chromatic colors highlight with step 9. Gray highlights with step 12 and inverts the text.

// Standard color
<Combobox.Root size="2" color="crimson">
  <Combobox.Trigger variant="soft" aria-label="Priority">
    <Combobox.Value placeholder="Select priority" />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="high">High Priority</Combobox.Item>
      <Combobox.Item value="medium">Medium Priority</Combobox.Item>
      <Combobox.Item value="low">Low Priority</Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>
 
// Gray highlights with step 12
<Combobox.Root size="2" color="gray">
  <Combobox.Trigger variant="soft" aria-label="Priority">
    <Combobox.Value placeholder="Select priority" />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="high">High Priority</Combobox.Item>
      <Combobox.Item value="medium">Medium Priority</Combobox.Item>
      <Combobox.Item value="low">Low Priority</Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

States

Loading

Set loading on Trigger while options are fetched. It shows a spinner, sets aria-busy, and keeps the popup closed.

<Combobox.Root size="2">
  <Combobox.Trigger variant="surface" aria-label="Option" loading>
    <Combobox.Value placeholder="Loading options..." />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="1">Option 1</Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Disabled

Set disabled on Root to prevent all interaction with the combobox.

<Combobox.Root size="2" disabled>
  <Combobox.Trigger variant="surface" aria-label="Option">
    <Combobox.Value placeholder="Disabled" />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="1">Option 1</Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Error

Use error on Trigger to indicate validation failures. It also sets aria-invalid.

<Combobox.Root size="2">
  <Combobox.Trigger variant="surface" aria-label="Option" error>
    <Combobox.Value placeholder="Invalid selection" />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="1">Option 1</Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Read Only

Use readOnly on Trigger to show the selected value without allowing changes. The trigger stays focusable and the popup does not open.

<Combobox.Root size="2" value="1" displayValue="Option 1">
  <Combobox.Trigger variant="surface" aria-label="Option" readOnly>
    <Combobox.Value placeholder="Read only" />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="1">Option 1</Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Groups and Labels

Organize options with groups and labels for better scannability in large lists. A Label inside a Group names that group for screen readers.

<Combobox.Root size="2">
  <Combobox.Trigger variant="surface" aria-label="Framework">
    <Combobox.Value placeholder="Select framework" />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input placeholder="Search frameworks..." />
    <Combobox.List>
      <Combobox.Group>
        <Combobox.Label>React Ecosystem</Combobox.Label>
        <Combobox.Item value="react">React</Combobox.Item>
        <Combobox.Item value="next">Next.js</Combobox.Item>
        <Combobox.Item value="remix">Remix</Combobox.Item>
      </Combobox.Group>
      <Combobox.Separator />
      <Combobox.Group>
        <Combobox.Label>Vue Ecosystem</Combobox.Label>
        <Combobox.Item value="vue">Vue</Combobox.Item>
        <Combobox.Item value="nuxt">Nuxt</Combobox.Item>
      </Combobox.Group>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Controlled State

Control the selection and search values for full state management. Search also works uncontrolled; control it only when you need the text.

const options = [
  { value: '1', label: 'Option 1' },
  { value: '2', label: 'Option 2' },
  { value: '3', label: 'Option 3' },
];
const getOptionLabel = (value: string | null) => options.find((option) => option.value === value)?.label;
 
function ControlledCombobox() {
  const [value, setValue] = React.useState<string | null>(null);
  const [searchValue, setSearchValue] = React.useState('');
 
  return (
    <Combobox.Root
      size="2"
      value={value}
      onValueChange={setValue}
      searchValue={searchValue}
      onSearchValueChange={setSearchValue}
      displayValue={getOptionLabel}
    >
      <Combobox.Trigger variant="surface" aria-label="Option">
        <Combobox.Value placeholder="Select..." />
      </Combobox.Trigger>
      <Combobox.Content>
        <Combobox.Input placeholder="Search..." />
        <Combobox.List>
          {options.map((option) => (
            <Combobox.Item key={option.value} value={option.value}>
              {option.label}
            </Combobox.Item>
          ))}
        </Combobox.List>
      </Combobox.Content>
    </Combobox.Root>
  );
}

Display Value

Use the displayValue prop on Root to show a label for the selected value. Without it, the trigger shows the raw value. Keep the function stable (module scope or useCallback) so it does not run on every render.

const countries = [
  { code: 'us', name: 'United States' },
  { code: 'uk', name: 'United Kingdom' },
  { code: 'ca', name: 'Canada' },
];
 
// Module scope: a stable function
const getCountryName = (value: string | null) => countries.find((c) => c.code === value)?.name;
 
function CountryPicker() {
  const [selectedCountry, setSelectedCountry] = React.useState<string | null>('us');
 
  return (
    <Combobox.Root
      size="2"
      value={selectedCountry}
      onValueChange={setSelectedCountry}
      displayValue={getCountryName}
    >
      <Combobox.Trigger variant="surface" aria-label="Country">
        <Combobox.Value placeholder="Select country" />
      </Combobox.Trigger>
      <Combobox.Content>
        <Combobox.Input placeholder="Search..." />
        <Combobox.List>
          {countries.map((country) => (
            <Combobox.Item key={country.code} value={country.code}>
              {country.name}
            </Combobox.Item>
          ))}
        </Combobox.List>
      </Combobox.Content>
    </Combobox.Root>
  );
}
 
// Using a static string
<Combobox.Root size="2" value="us" displayValue="United States">
  <Combobox.Trigger variant="surface" aria-label="Country">
    <Combobox.Value />
  </Combobox.Trigger>
  {/* ... */}
</Combobox.Root>

Custom Filtering

Disable Built-in Filtering

For server-side search, disable the built-in filtering and fetch options with a data library such as React Query.

import { useQuery, keepPreviousData } from '@tanstack/react-query';
 
function AsyncCombobox() {
  const [searchValue, setSearchValue] = React.useState('');
  const deferredSearch = React.useDeferredValue(searchValue);
 
  const { data: options = [], isFetching } = useQuery({
    queryKey: ['options', deferredSearch],
    queryFn: () => fetchOptions(deferredSearch), // your typed API client
    placeholderData: keepPreviousData,
  });
 
  return (
    <Combobox.Root
      size="2"
      shouldFilter={false}
      searchValue={searchValue}
      onSearchValueChange={setSearchValue}
    >
      <Combobox.Trigger variant="surface" aria-label="Option">
        <Combobox.Value placeholder="Search..." />
      </Combobox.Trigger>
      <Combobox.Content>
        <Combobox.Input placeholder="Type to search..." />
        <Combobox.List>
          {options.map((option) => (
            <Combobox.Item key={option.id} value={option.id}>
              {option.label}
            </Combobox.Item>
          ))}
          <Combobox.Empty>{isFetching ? 'Searching...' : 'No results'}</Combobox.Empty>
        </Combobox.List>
      </Combobox.Content>
    </Combobox.Root>
  );
}

Custom Filter Function

Provide custom ranking logic for more control over search results. The item's visible text is passed as the first keyword.

const customFilter = (value: string, search: string, keywords?: string[]) => {
  const searchLower = search.toLowerCase();
  const terms = [value, ...(keywords ?? [])].map((term) => term.toLowerCase());
 
  // Exact match gets highest score
  if (terms.some((term) => term === searchLower)) return 1;
 
  // Starts with search gets high score
  if (terms.some((term) => term.startsWith(searchLower))) return 0.8;
 
  // Contains search gets medium score
  if (terms.some((term) => term.includes(searchLower))) return 0.5;
 
  return 0; // No match
};
 
<Combobox.Root size="2" filter={customFilter}>
  <Combobox.Trigger variant="surface" aria-label="Library">
    <Combobox.Value placeholder="Select..." />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="react" keywords={['library', 'ui', 'frontend']}>
        React
      </Combobox.Item>
      <Combobox.Item value="vue" keywords={['framework', 'ui', 'frontend']}>
        Vue
      </Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Empty State

Use Combobox.Empty to provide helpful feedback when no results match the search.

<Combobox.Root size="2">
  <Combobox.Trigger variant="surface" aria-label="Framework">
    <Combobox.Value placeholder="Select..." />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="react">React</Combobox.Item>
      <Combobox.Item value="vue">Vue</Combobox.Item>
      <Combobox.Item value="angular">Angular</Combobox.Item>
      <Combobox.Empty>No options found. Try a different search.</Combobox.Empty>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Input Adornments

Add icons or elements before or after the search input for better visual context.

import { HugeiconsIcon } from '@hugeicons/react';
import { Search01Icon } from '@hugeicons/core-free-icons';
 
<Combobox.Root size="2">
  <Combobox.Trigger variant="surface" aria-label="Option">
    <Combobox.Value placeholder="Select..." />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input
      placeholder="Search..."
      startAdornment={<HugeiconsIcon icon={Search01Icon} strokeWidth={1.75} />}
    />
    <Combobox.List>
      <Combobox.Item value="1">Option 1</Combobox.Item>
      <Combobox.Item value="2">Option 2</Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Responsive

Use responsive objects with size on Root to adapt sizing across different breakpoints.

<Combobox.Root size={{ initial: '1', sm: '2', md: '3' }}>
  <Combobox.Trigger variant="surface" aria-label="Option">
    <Combobox.Value placeholder="Responsive size" />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="1">Option 1</Combobox.Item>
      <Combobox.Item value="2">Option 2</Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Material

Use material on Trigger for the trigger background and on Content for the dropdown surface. Choose solid for opaque backgrounds, or translucent for depth over images or dynamic backgrounds. Content falls back to the Theme material.

<Combobox.Root size="2">
  <Combobox.Trigger variant="soft" aria-label="Option" material="translucent">
    <Combobox.Value placeholder="Translucent" />
  </Combobox.Trigger>
  <Combobox.Content material="translucent">
    <Combobox.Input placeholder="Search..." />
    <Combobox.List>
      <Combobox.Item value="1">Option 1</Combobox.Item>
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Accessibility

The trigger follows the WAI-ARIA combobox pattern with a dialog popup. The popup holds a search field (itself a combobox) that controls the listbox of options.

Labelling

The combobox role does not take its name from its content, so the trigger needs a label. Use aria-label, aria-labelledby, or a <label htmlFor> pointing at the trigger. The same name labels the popup, the search field and the listbox. In development, an unlabelled trigger logs a warning.

<Flex direction="column" gap="1">
  <Text as="label" size="2" weight="medium" htmlFor="country">
    Country
  </Text>
  <Combobox.Root size="2">
    <Combobox.Trigger id="country" variant="surface">
      <Combobox.Value placeholder="Select country" />
    </Combobox.Trigger>
    <Combobox.Content>
      <Combobox.Input placeholder="Search..." />
      <Combobox.List>
        <Combobox.Item value="us">United States</Combobox.Item>
        <Combobox.Item value="uk">United Kingdom</Combobox.Item>
      </Combobox.List>
    </Combobox.Content>
  </Combobox.Root>
</Flex>

Keyboard Navigation

  • Space/Enter/Arrow Down - Open the dropdown when the trigger is focused
  • Type - Filter options in real time
  • Arrow Down/Up - Move the highlight through options
  • Home/End - Jump to first/last option
  • Enter - Select the highlighted option and close
  • Escape - Close without selecting
  • Tab - Close the dropdown and move focus to the next element

When the dropdown opens, the highlight starts on the selected option. Without a Combobox.Input, focus moves to the list so the keys above still work.

ARIA Attributes

  • role="combobox" with aria-haspopup="dialog", aria-expanded and aria-controls on the trigger
  • aria-invalid, aria-busy, aria-readonly and aria-disabled follow the error, loading, readOnly and disabled props
  • The search field has aria-controls pointing at the listbox and aria-activedescendant pointing at the highlighted option
  • aria-selected marks the highlighted option; aria-checked marks the chosen option

Screen Readers

  • The number of matching results, or "No results", is announced after typing pauses
  • The chosen option is exposed with aria-checked
  • Group labels name their groups
  • The loading state is exposed with aria-busy

Focus Management

  • The search field receives focus when the dropdown opens
  • Focus returns to the trigger when the dropdown closes
  • In Windows High Contrast mode, the highlighted option shows a system outline

Changelog

Added

  • Initial Combobox component with search and filtering
  • Built on cmdk for fuzzy search and keyboard navigation
    • Trigger variants: classic, surface, soft, outline, ghost
    • Content variants: solid, soft
  • Three sizes (1, 2, 3) with responsive support
  • Custom filter function support via filter prop
  • displayValue prop for showing labels without mounting items
  • Groups, labels, and separators for organization
  • Empty state component for no-results feedback
  • Loading, error, disabled, and read-only states
  • Input adornments (startAdornment, endAdornment)
  • Full controlled state support (value, searchValue, open)
  • resetSearchOnSelect and resetSearchOnClose options to control search clearing
  • material prop on Content
  • Result count announcements for screen readers
  • Color prop for semantic styling
  • Margin props with responsive support
  • Windows High Contrast mode support
  • Reduced motion support for animations

Removed

  • highContrast prop. Gray highlights with step 12 on its own, and text reads the strong ink by default.

Fixed

  • Uncontrolled search can be typed into
  • Search matches the item's visible text, not only its value
  • Typing, hovering and selecting no longer re-render every item
  • Hover and keyboard highlight use one source, so only one option is lit
  • Opening highlights the selected option
  • Responsive size builds valid class names for the search field
  • readOnly and loading keep the popup closed
  • Trigger ARIA matches the dialog popup and aria-controls points at it
  • Performance optimization with lazy mounting (forceMount control)
  • Focus management when opening/closing dropdown

© 2026 Kushagra Dhawan. Licensed under MIT. GitHub.

Theme

Accent color

Gray color

Appearance

Radius

Scaling

Panel background