kosori

Combobox

A searchable select for choosing one option or many.

Combobox composes Base UI's Combobox primitive with Input and Scroll Area, so filtering, grouping and the popup share one model. Every part carries a data-slot for styling.

Installation

npx shadcn@latest add @kosori/combobox

Example

The Example below is a registry:example item, built and validated by the same pipeline as Combobox. Its source is the file @kosori/combobox-example installs, so the preview and the install cannot drift.

combobox-example.tsx
combobox-example
'use client';

import { Fragment } from 'react';
import {
  Combobox,
  ComboboxCollection,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxGroupLabel,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxPopup,
  ComboboxSeparator,
} from '@/components/ui/combobox';

type Tag = { group: 'Priority' | 'Status' | 'Team'; id: string; label: string };
type TagGroup = { items: Tag[]; value: Tag['group'] };

const tagsData: Tag[] = [
  { group: 'Status', id: 's-open', label: 'Open' },
  { group: 'Status', id: 's-in-progress', label: 'In progress' },
  { group: 'Status', id: 's-blocked', label: 'Blocked' },
  { group: 'Status', id: 's-resolved', label: 'Resolved' },
  { group: 'Status', id: 's-closed', label: 'Closed' },
  { group: 'Priority', id: 'p-low', label: 'Low' },
  { group: 'Priority', id: 'p-medium', label: 'Medium' },
  { group: 'Priority', id: 'p-high', label: 'High' },
  { group: 'Priority', id: 'p-urgent', label: 'Urgent' },
  { group: 'Team', id: 't-design', label: 'Design' },
  { group: 'Team', id: 't-frontend', label: 'Frontend' },
  { group: 'Team', id: 't-backend', label: 'Backend' },
  { group: 'Team', id: 't-devops', label: 'DevOps' },
  { group: 'Team', id: 't-qa', label: 'QA' },
  { group: 'Team', id: 't-mobile', label: 'Mobile' },
  { group: 'Team', id: 't-data', label: 'Data' },
  { group: 'Team', id: 't-security', label: 'Security' },
  { group: 'Team', id: 't-platform', label: 'Platform' },
  { group: 'Team', id: 't-infra', label: 'Infrastructure' },
  { group: 'Team', id: 't-product', label: 'Product' },
  { group: 'Team', id: 't-marketing', label: 'Marketing' },
  { group: 'Team', id: 't-sales', label: 'Sales' },
  { group: 'Team', id: 't-support', label: 'Support' },
  { group: 'Team', id: 't-research', label: 'Research' },
  { group: 'Team', id: 't-content', label: 'Content' },
  { group: 'Team', id: 't-analytics', label: 'Analytics' },
  { group: 'Team', id: 't-operations', label: 'Operations' },
  { group: 'Team', id: 't-finance', label: 'Finance' },
  { group: 'Team', id: 't-hr', label: 'HR' },
  { group: 'Team', id: 't-legal', label: 'Legal' },
  { group: 'Team', id: 't-growth', label: 'Growth' },
  { group: 'Team', id: 't-partner', label: 'Partner' },
  { group: 'Team', id: 't-community', label: 'Community' },
  { group: 'Team', id: 't-docs', label: 'Docs' },
  { group: 'Team', id: 't-l10n', label: 'Localization' },
  { group: 'Team', id: 't-a11y', label: 'Accessibility' },
  { group: 'Team', id: 't-sre', label: 'SRE' },
  { group: 'Team', id: 't-release', label: 'Release' },
  { group: 'Team', id: 't-architecture', label: 'Architecture' },
  { group: 'Team', id: 't-ux', label: 'UX' },
  { group: 'Team', id: 't-ui', label: 'UI' },
  { group: 'Team', id: 't-management', label: 'Management' },
];

const groupTags = (tags: Tag[]): TagGroup[] => {
  const groups: Record<string, Tag[]> = {};
  for (const tag of tags) {
    if (!groups[tag.group]) {
      groups[tag.group] = [];
    }
    groups[tag.group]?.push(tag);
  }

  const order: TagGroup['value'][] = ['Status', 'Priority', 'Team'];
  return order.map((value) => ({ items: groups[value] ?? [], value }));
};

const groupedTags: TagGroup[] = groupTags(tagsData);

const ComboboxExample = () => (
  <Combobox items={groupedTags}>
    <div className='flex flex-col items-start gap-2'>
      <ComboboxInput aria-label='Search tags' placeholder='e.g. feature' />
    </div>
    <ComboboxPopup>
      <ComboboxEmpty>No tags found.</ComboboxEmpty>
      <ComboboxList>
        {(group: TagGroup) => (
          <Fragment key={group.value}>
            <ComboboxGroup items={group.items}>
              <ComboboxGroupLabel>{group.value}</ComboboxGroupLabel>
              <ComboboxCollection>
                {(tag: Tag) => (
                  <ComboboxItem key={tag.id} value={tag}>
                    {tag.label}
                  </ComboboxItem>
                )}
              </ComboboxCollection>
            </ComboboxGroup>
            {group.value !== 'Team' && <ComboboxSeparator />}
          </Fragment>
        )}
      </ComboboxList>
    </ComboboxPopup>
  </Combobox>
);

export default ComboboxExample;

Usage

import {
  Combobox,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxPopup,
} from '@/components/ui/combobox';

const fruits = ['Apple', 'Banana', 'Orange'];

const ComboboxDemo = () => (
  <Combobox items={fruits}>
    <ComboboxInput placeholder='Search fruit' />
    <ComboboxPopup>
      <ComboboxEmpty>No fruit found.</ComboboxEmpty>
      <ComboboxList>
        {(fruit: string) => (
          <ComboboxItem value={fruit}>{fruit}</ComboboxItem>
        )}
      </ComboboxList>
    </ComboboxPopup>
  </Combobox>
);

On this page