Components

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.View source →

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

shell
npm install @kushagradhawan/kookie-ui

Usage

tsx
import { Combobox } from '@kushagradhawan/kookie-ui';
 
export function MyComponent() {
  return (
    <Combobox.Root>
      <Combobox.Trigger>
        <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.Item value="3">Option 3</Combobox.Item>
        </Combobox.List>
      </Combobox.Content>
    </Combobox.Root>
  );
}

Anatomy

Combobox is a compound component with the following parts:

tsx
<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 (32px) for toolbars, 2 (40px) for standard forms, 3 (48px) for prominent selections. Supports responsive objects
highContrastbooleanMaximum visibility mode for trigger and items, especially useful on complex backgrounds
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 options...")
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
resetSearchOnSelectbooleanWhether to clear search when an option is selected (default: true)
colorAccentColorSemantic color for the trigger and selected items
displayValuestring | ((value) => string)Display value shown in trigger. Use this for best performance instead of relying on item label registration. Can be a static string or function that takes the current value

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'Background appearance: solid for opaque, translucent for depth over images
panelBackground'solid' | 'translucent'Deprecated: Use material instead
loadingbooleanShows spinner and disables trigger automatically
errorbooleanVisual error state, typically for validation failures
readOnlybooleanRead-only state - displays but doesn't allow changes
disabledbooleanPrevents interaction. Overrides Root disabled if provided
widthstringTrigger width (CSS value)
placeholderstringOverride placeholder from Root

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
highContrastbooleanOverride highContrast from Root
forceMountbooleanKeep content mounted when closed. Use displayValue prop on Root instead for better performance

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

Other Components

  • Combobox.Value - Displays selected option label or placeholder in trigger
  • Combobox.List - Container for items with ScrollArea support
  • Combobox.Item - Selectable option. Requires value prop
  • Combobox.Group - Groups related items together
  • Combobox.Label - Non-interactive label for groups
  • 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.

tsx
<Combobox.Root size="2">
  <Combobox.Trigger variant="classic">
    <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.

tsx
      <Combobox.Root size="2">
        <Combobox.Trigger variant="surface">
          <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.

tsx
      <Combobox.Root size="2">
        <Combobox.Trigger variant="soft">
          <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.

tsx
      <Combobox.Root size="2">
        <Combobox.Trigger variant="outline">
          <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.

tsx
      <Combobox.Root size="2">
        <Combobox.Trigger variant="ghost">
          <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.

tsx
      <Combobox.Root size="2">
        <Combobox.Trigger variant="surface">
          <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.

tsx
<Combobox.Root size="2">
  <Combobox.Trigger variant="surface">
    <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.

tsx
// Size 1 - 32px height, for toolbars and dense interfaces
      <Combobox.Root size="1">
        <Combobox.Trigger variant="surface">
          <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 - 40px height, standard for most forms
      <Combobox.Root size="2">
        <Combobox.Trigger variant="surface">
          <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 - 48px height, for prominent selections and mobile touch targets
      <Combobox.Root size="3">
        <Combobox.Trigger variant="surface">
          <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. Use highContrast for maximum visibility on complex backgrounds.

tsx
// Standard color
      <Combobox.Root size="2" color="crimson">
        <Combobox.Trigger variant="soft">
          <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>
 
// High contrast mode for maximum visibility
      <Combobox.Root size="2" color="crimson" highContrast>
        <Combobox.Trigger variant="soft">
          <Combobox.Value placeholder="High contrast" />
        </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 to show a spinner during async operations like fetching options.

tsx
      <Combobox.Root size="2">
        <Combobox.Trigger variant="surface" 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.

tsx
      <Combobox.Root size="2" disabled>
        <Combobox.Trigger variant="surface">
          <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.

tsx
      <Combobox.Root size="2">
        <Combobox.Trigger variant="surface" 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.

tsx
<Combobox.Root size="2" value="1">
  <Combobox.Trigger variant="surface" 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.

tsx
<Combobox.Root size="2">
  <Combobox.Trigger variant="surface">
    <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.

tsx
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}
    >
      <Combobox.Trigger variant="surface">
        <Combobox.Value placeholder="Select..." />
      </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.Item value="3">Option 3</Combobox.Item>
        </Combobox.List>
      </Combobox.Content>
    </Combobox.Root>
  );
}

Display Value

Use the displayValue prop on Root for best performance when you have access to the selected item's label from your data source. This avoids needing to mount all items just to register their labels.

tsx
// Using a function (recommended)
function ComboboxWithDisplayValue() {
  const [selectedCountry, setSelectedCountry] = React.useState<string | null>('us');
  const countries = [
    { code: 'us', name: 'United States' },
    { code: 'uk', name: 'United Kingdom' },
    { code: 'ca', name: 'Canada' },
  ];
 
  return (
    <Combobox.Root
      size="2"
      value={selectedCountry}
      onValueChange={setSelectedCountry}
      displayValue={(value) => countries.find(c => c.code === value)?.name}
    >
      <Combobox.Trigger variant="surface">
        <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">
    <Combobox.Value />
  </Combobox.Trigger>
  {/* ... */}
</Combobox.Root>

Custom Filtering

Disable Built-in Filtering

For async or server-side search, disable the built-in filtering and manage options yourself.

tsx
function AsyncCombobox() {
  const [searchValue, setSearchValue] = React.useState('');
  const [options, setOptions] = React.useState([]);
 
  React.useEffect(() => {
    // Fetch options based on search value
    fetchOptions(searchValue).then(setOptions);
  }, [searchValue]);
 
  return (
    <Combobox.Root
      size="2"
      shouldFilter={false}
      searchValue={searchValue}
      onSearchValueChange={setSearchValue}
    >
      <Combobox.Trigger variant="surface">
        <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.List>
      </Combobox.Content>
    </Combobox.Root>
  );
}

Custom Filter Function

Provide custom ranking logic for more control over search results.

tsx
const customFilter = (value: string, search: string, keywords?: string[]) => {
  const searchLower = search.toLowerCase();
  const valueLower = value.toLowerCase();
  
  // Exact match gets highest score
  if (valueLower === searchLower) return 1;
  
  // Starts with search gets high score
  if (valueLower.startsWith(searchLower)) return 0.8;
  
  // Contains search gets medium score
  if (valueLower.includes(searchLower)) return 0.5;
  
  // Check keywords if provided
  if (keywords) {
    const keywordMatch = keywords.some(keyword => 
      keyword.toLowerCase().includes(searchLower)
    );
    if (keywordMatch) return 0.3;
  }
  
  return 0; // No match
};
 
<Combobox.Root size="2" filter={customFilter}>
  <Combobox.Trigger variant="surface">
    <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.

tsx
<Combobox.Root size="2">
  <Combobox.Trigger variant="surface">
    <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.

tsx
import { Search } from 'lucide-react';
 
<Combobox.Root size="2">
  <Combobox.Trigger variant="surface">
    <Combobox.Value placeholder="Select..." />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input 
      placeholder="Search..." 
      startAdornment={<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>

Responsive

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

tsx
<Combobox.Root size={{ initial: '1', sm: '2', md: '3' }}>
  <Combobox.Trigger variant="surface">
    <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 the material prop on Trigger to control background appearance. Choose solid for opaque backgrounds, or translucent for depth over images or dynamic backgrounds.

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

Accessibility

Combobox implements the WAI-ARIA 1.2 combobox pattern with comprehensive keyboard support.

Keyboard Navigation

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

ARIA Attributes

  • role="combobox" - Identifies the trigger as a combobox
  • aria-expanded - Announces dropdown state (open/closed)
  • aria-haspopup="listbox" - Indicates dropdown contains a listbox
  • aria-controls - Links trigger to listbox when open
  • aria-activedescendant - Announces currently highlighted option
  • aria-autocomplete="list" - Indicates filtering behavior
  • aria-disabled - Announces disabled state

Screen Readers

  • Selection changes are announced automatically
  • Search results update live as user types
  • Empty state is announced when no matches found
  • Loading state communicated through spinner

Focus Management

  • Input automatically receives focus when dropdown opens
  • Focus returns to trigger when dropdown closes
  • Keyboard navigation maintains visual highlight

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 optimal performance with large lists
  • 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 option to control search clearing
  • Color and highContrast props for semantic styling
  • Material prop for translucent effects
  • Margin props with responsive support
  • RTL (Right-to-Left) layout support
  • Windows High Contrast mode support
  • Reduced motion support for animations

Fixed

  • Label registration system for displaying selected value
  • Performance optimization with lazy mounting (forceMount control)
  • ARIA attributes for WAI-ARIA 1.2 combobox pattern
  • Focus management when opening/closing dropdown
  • Search value synchronization with controlled state
© 2026 Kushagra Dhawan. Licensed under MIT. GitHub.

Theme

Accent color

Gray color

Appearance

Radius

Scaling

Panel background