# Combobox

An accessible React combobox for Next.js and Tailwind CSS: type in the field to filter and pick one option, or several as chips, with a gliding highlight.

Source: https://ui.fewtone.com/components/combobox

## Installation

```bash
npx shadcn@latest add https://ui.fewtone.com/r/combobox.json
```

## Usage

```tsx
import { Combobox } from "@/components/ui/combobox"

const frameworks = [
  { value: "next", label: "Next.js" },
  { value: "astro", label: "Astro" },
  { value: "nuxt", label: "Nuxt", keywords: ["vue"] },
]

export function FrameworkPicker() {
  return (
    <Combobox
      aria-label="Framework"
      options={frameworks}
      placeholder="Pick a framework"
      emptyText="No framework found."
    />
  )
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `options` | `{ value: string; label: string; keywords?: string[]; disabled?: boolean; group?: string }[]` | – | The options. The search matches the label and keywords; options with the same group are listed under it. |
| `multiple` | `boolean` | `false` | Pick several, shown as chips. The value is then a string[]. |
| `value` | `string \| string[]` | – | The chosen value (or values, with multiple), when you control it. An empty string is none. |
| `defaultValue` | `string \| string[]` | – | The value chosen at first. |
| `onValueChange` | `(value: string \| string[]) => void` | – | Called when the choice changes: a string, or a string[] with multiple. |
| `placeholder` | `string` | – | The hint in the empty field. |
| `emptyText` | `ReactNode` | `"No results found."` | Shown when nothing matches. |
| `renderOption` | `(option) => ReactNode` | – | Custom content for a row in the list. The field still shows the label. |
| `size` | `"sm" \| "md"` | `"md"` | The height of one line: Select's small and medium. |
| `name` | `string` | – | Submits the value with a form (one hidden input per value with multiple). |
| `filter` | `(text: string, search: string, keywords?: string[]) => number` | – | Scores an option against the search; 0 hides it. Defaults to Command's ranking. |
| `onSearchChange` | `(search: string) => void` | – | Called with the search as it changes. |
| `disabled` | `boolean` | `false` | Prevents typing and opening. |
| `aria-invalid` | `boolean` | – | Marks the value as wrong: a destructive border and focus ring. |

**When to use it:** Use Combobox to pick one value, or several, from a long list by typing, such as a framework, a country or labels. Use Select for a short list without typing, Radio Group for 2 to 4 visible options, and Command for a menu of pages and actions.
**Built on:** Radix Popover, Command.
**Accessibility pattern:** https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-list/
**npm packages (installed by the CLI):** lucide-react@^1.52.0, motion@^14.0.0, radix-ui@^1.7.0.
**Installs these FewTone UI components with it:** `command`.

## Examples

### One value or several

Type in the field to filter, use the arrow keys or the pointer to move, and Enter or a click to pick. With multiple, choices become chips: the list stays open, Backspace in an empty field removes the last one, and the field grows smoothly when they wrap.

```tsx
"use client";

import { Combobox, type ComboboxOption } from "@/components/ui/combobox";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { useId, useState } from "react";

type Size = "sm" | "md";

const frameworks: ComboboxOption[] = [
  { value: "next", label: "Next.js", keywords: ["react", "vercel"] },
  { value: "react-router", label: "React Router", keywords: ["remix", "react"] },
  { value: "astro", label: "Astro" },
  { value: "nuxt", label: "Nuxt", keywords: ["vue"] },
  { value: "sveltekit", label: "SvelteKit", keywords: ["svelte"] },
  { value: "solid-start", label: "SolidStart", keywords: ["solid"] },
  { value: "tanstack-start", label: "TanStack Start", keywords: ["react", "tanstack"] },
];

const labels: ComboboxOption[] = [
  { value: "bug", label: "Bug" },
  { value: "feature", label: "Feature" },
  { value: "docs", label: "Docs", keywords: ["documentation"] },
  { value: "design", label: "Design" },
  { value: "performance", label: "Performance", keywords: ["speed", "perf"] },
  { value: "accessibility", label: "Accessibility", keywords: ["a11y"] },
  { value: "good-first-issue", label: "Good first issue" },
  { value: "help-wanted", label: "Help wanted" },
];

/*
 * One value and several side by side, each named underneath, with the size in the top-right corner. An empty
 * row at the bottom, as tall as the setting's row, keeps them centred.
 */
export default function ComboboxPickers() {
  const id = useId();
  const [size, setSize] = useState<Size>("md");

  return (
    <div className="flex w-full flex-col gap-6 self-stretch">
      <div className="-mt-6 -mr-6 flex items-center justify-end gap-2">
        <label htmlFor={`${id}-size`} className="text-sm text-muted-foreground">
          Size
        </label>
        <Select value={size} onValueChange={(value) => setSize(value as Size)}>
          <SelectTrigger id={`${id}-size`} size="sm" className="w-28">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="sm">Small</SelectItem>
            <SelectItem value="md">Medium</SelectItem>
          </SelectContent>
        </Select>
      </div>

      <div className="grid flex-1 content-center justify-center gap-x-10 gap-y-6 sm:grid-cols-2">
        <div className="grid w-64 max-w-full content-start gap-2">
          <label htmlFor={`${id}-framework`} className="text-sm font-medium">
            Framework
          </label>
          <Combobox
            id={`${id}-framework`}
            size={size}
            options={frameworks}
            defaultValue="next"
            placeholder="Pick a framework"
            emptyText="No framework found."
          />
          <span className="text-xs text-muted-foreground">One value</span>
        </div>
        <div className="grid w-64 max-w-full content-start gap-2">
          <label htmlFor={`${id}-labels`} className="text-sm font-medium">
            Labels
          </label>
          <Combobox
            id={`${id}-labels`}
            multiple
            size={size}
            options={labels}
            defaultValue={["bug", "design"]}
            placeholder="Add labels"
            emptyText="No label found."
          />
          <span className="text-xs text-muted-foreground">Several, as chips</span>
        </div>
      </div>

      <div aria-hidden className="-mb-6 h-(--size-control-sm)" />
    </div>
  );
}
```

### Groups and long lists

Options with a group are listed under it as a heading, and keywords widen the search: try IST or Pacific. renderOption adds detail to each row while the field shows the label. Long lists scroll, with soft fades at the ends.

```tsx
"use client";

import { Combobox, type ComboboxOption } from "@/components/ui/combobox";
import { useId } from "react";

/** City, region, country, and the abbreviations and other names people search for */
const zones: [string, string, string, string, string[]][] = [
  ["America/Los_Angeles", "Los Angeles", "Americas", "United States", ["PST", "Pacific", "San Francisco"]],
  ["America/Denver", "Denver", "Americas", "United States", ["MST", "Mountain"]],
  ["America/Chicago", "Chicago", "Americas", "United States", ["CST", "Central"]],
  ["America/New_York", "New York", "Americas", "United States", ["EST", "Eastern"]],
  ["America/Toronto", "Toronto", "Americas", "Canada", ["EST", "Eastern"]],
  ["America/Sao_Paulo", "São Paulo", "Americas", "Brazil", ["BRT", "Sao Paulo"]],
  ["Europe/London", "London", "Europe and Africa", "United Kingdom", ["GMT", "BST", "UK"]],
  ["Europe/Paris", "Paris", "Europe and Africa", "France", ["CET"]],
  ["Europe/Berlin", "Berlin", "Europe and Africa", "Germany", ["CET"]],
  ["Africa/Lagos", "Lagos", "Europe and Africa", "Nigeria", ["WAT"]],
  ["Africa/Nairobi", "Nairobi", "Europe and Africa", "Kenya", ["EAT"]],
  ["Asia/Dubai", "Dubai", "Asia and Pacific", "United Arab Emirates", ["GST", "UAE"]],
  ["Asia/Kolkata", "Kolkata", "Asia and Pacific", "India", ["IST", "Mumbai", "Delhi", "Bengaluru"]],
  ["Asia/Singapore", "Singapore", "Asia and Pacific", "Singapore", ["SGT"]],
  ["Asia/Tokyo", "Tokyo", "Asia and Pacific", "Japan", ["JST"]],
  ["Australia/Sydney", "Sydney", "Asia and Pacific", "Australia", ["AEST", "AEDT"]],
  ["Pacific/Auckland", "Auckland", "Asia and Pacific", "New Zealand", ["NZST", "NZDT"]],
];

const options: ComboboxOption[] = zones.map(([value, label, group, country, keywords]) => ({
  value,
  label,
  group,
  keywords: [country, ...keywords],
}));

const countries = new Map(zones.map(([value, , , country]) => [value, country]));

/*
 * Options grouped under headings, with keywords for the abbreviations people type (IST, Pacific). Each row
 * also shows the country; the field still shows the city. The list scrolls, with soft fades at the ends.
 * A client file, because renderOption is a function.
 */
export default function ComboboxGroups() {
  const id = useId();
  return (
    <div className="grid w-64 max-w-full gap-2">
      <label htmlFor={id} className="text-sm font-medium">
        Time zone
      </label>
      <Combobox
        id={id}
        options={options}
        defaultValue="Asia/Kolkata"
        placeholder="Pick a time zone"
        emptyText="No time zone found."
        renderOption={(option) => (
          <span className="flex min-w-0 flex-1 items-baseline justify-between gap-3">
            <span className="truncate">{option.label}</span>
            <span className="truncate text-xs text-muted-foreground">{countries.get(option.value)}</span>
          </span>
        )}
      />
    </div>
  );
}
```

## Related

- [Select](https://ui.fewtone.com/components/select.md): An accessible React select with a gliding highlight, groups, labels and a scrollable list, built on Radix Select.
- [Command](https://ui.fewtone.com/components/command.md): An accessible React command menu for Next.js and Tailwind CSS: a ⌘K palette that filters and ranks as you type, with groups, shortcuts and a gliding highlight.
- [Input](https://ui.fewtone.com/components/input.md): An accessible React text input with three sizes and clear invalid, disabled and file states, built on a native input.
