# Combobox

Filters large lists to selectable options based on the matching query.

---

## Uncontrolled

```tsx
import {
  Combobox,
  ComboboxInput,
  ComboboxList,
  ComboboxOption,
} from '@vercel/geistcn/components';
import type { JSX } from 'react';

export function Component(): JSX.Element {
  return (
    <Combobox aria-label="Search" placeholder="Search...">
      <ComboboxInput />
      <ComboboxList>
        <ComboboxOption value="a">One</ComboboxOption>
        <ComboboxOption value="b">Two</ComboboxOption>
        <ComboboxOption value="c">Three</ComboboxOption>
      </ComboboxList>
    </Combobox>
  );
}
```

## Controlled

```tsx
import { useState, type JSX } from 'react';
import {
  Combobox,
  ComboboxInput,
  ComboboxList,
  ComboboxOption,
} from '@vercel/geistcn/components';

export function Component(): JSX.Element {
  const [value, setValue] = useState<string | null>('b');

  return (
    <Combobox
      aria-label="Search"
      onChange={setValue}
      placeholder="Search..."
      value={value}
    >
      <ComboboxInput />
      <ComboboxList>
        <ComboboxOption value="a">One</ComboboxOption>
        <ComboboxOption value="b">Two</ComboboxOption>
        <ComboboxOption value="c">Three</ComboboxOption>
      </ComboboxList>
    </Combobox>
  );
}
```

## Disabled

```tsx
import {
  Combobox,
  ComboboxInput,
  ComboboxList,
  ComboboxOption,
} from '@vercel/geistcn/components';
import type { JSX } from 'react';

export function Component(): JSX.Element {
  return (
    <Combobox aria-label="Search" disabled placeholder="Search...">
      <ComboboxInput />
      <ComboboxList>
        <ComboboxOption value="a">One</ComboboxOption>
        <ComboboxOption value="b">Two</ComboboxOption>
        <ComboboxOption value="c">Three</ComboboxOption>
      </ComboboxList>
    </Combobox>
  );
}
```

## Errored

```tsx
import {
  Combobox,
  ComboboxInput,
  ComboboxList,
  ComboboxOption,
} from '@vercel/geistcn/components';
import type { JSX } from 'react';

export function Component(): JSX.Element {
  return (
    <Combobox aria-label="Search" errored placeholder="Search...">
      <ComboboxInput />
      <ComboboxList>
        <ComboboxOption value="a">One</ComboboxOption>
        <ComboboxOption value="b">Two</ComboboxOption>
        <ComboboxOption value="c">Three</ComboboxOption>
      </ComboboxList>
    </Combobox>
  );
}
```

## Custom width input

```tsx
import {
  Combobox,
  ComboboxInput,
  ComboboxList,
  ComboboxOption,
} from '@vercel/geistcn/components';
import type { JSX } from 'react';

export function Component(): JSX.Element {
  return (
    <Combobox aria-label="Search" placeholder="Search..." width={256}>
      <ComboboxInput />
      <ComboboxList>
        <ComboboxOption value="a">One</ComboboxOption>
        <ComboboxOption value="b">Two</ComboboxOption>
        <ComboboxOption value="c">Three</ComboboxOption>
      </ComboboxList>
    </Combobox>
  );
}
```

## Custom width list

```tsx
import {
  Combobox,
  ComboboxInput,
  ComboboxList,
  ComboboxOption,
} from '@vercel/geistcn/components';
import type { JSX } from 'react';

export function Component(): JSX.Element {
  return (
    <Combobox aria-label="Search" placeholder="Search...">
      <ComboboxInput />
      <ComboboxList maxWidth={500}>
        <ComboboxOption value="a">
          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
          eiusmod tempor incididunt ut labore et dolore magna aliqua.{' '}
        </ComboboxOption>
        <ComboboxOption value="b">
          Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
          nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
          reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
          pariatur.
        </ComboboxOption>
        <ComboboxOption value="c">
          Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
          officia deserunt mollit anim id est laborum.
        </ComboboxOption>
      </ComboboxList>
    </Combobox>
  );
}
```

## Custom empty message

```tsx
import {
  Combobox,
  ComboboxInput,
  ComboboxList,
} from '@vercel/geistcn/components';
import type { JSX } from 'react';

export function Component(): JSX.Element {
  return (
    <Combobox aria-label="Search" placeholder="Search..." width={256}>
      <ComboboxInput />
      <ComboboxList emptyMessage="Nothing to see here..." />
    </Combobox>
  );
}
```

## Clearable

Set `clearable` to show a clear button when a value is selected.

```tsx
import { useState, type JSX } from 'react';
import {
  Combobox,
  ComboboxInput,
  ComboboxList,
  ComboboxOption,
} from '@vercel/geistcn/components';

export function Component(): JSX.Element {
  const [value, setValue] = useState<string | null>('two');

  return (
    <Combobox
      aria-label="Search"
      clearable
      onChange={setValue}
      placeholder="Search..."
      value={value}
    >
      <ComboboxInput />
      <ComboboxList>
        <ComboboxOption value="one">one</ComboboxOption>
        <ComboboxOption value="two">two</ComboboxOption>
        <ComboboxOption value="three">three</ComboboxOption>
      </ComboboxList>
    </Combobox>
  );
}
```

## With prefix icons

```tsx
import type { JSX } from 'react';
import {
  Combobox,
  ComboboxInput,
  ComboboxList,
  ComboboxOption,
} from '@vercel/geistcn/components';
import { LogoIconVercelSvg } from '@vercel/geistcn-assets/logos';

export function Component(): JSX.Element {
  return (
    <Combobox aria-label="Search" placeholder="Search..." className="w-fit">
      <ComboboxInput />
      <ComboboxList>
        <ComboboxOption value="a" prefix={<LogoIconVercelSvg />}>
          One
        </ComboboxOption>
        <ComboboxOption value="b" prefix={<LogoIconVercelSvg />}>
          Two
        </ComboboxOption>
        <ComboboxOption value="c" prefix={<LogoIconVercelSvg />}>
          Three
        </ComboboxOption>
      </ComboboxList>
    </Combobox>
  );
}
```

## With suffix icons

```tsx
import type { JSX } from 'react';
import {
  Combobox,
  ComboboxInput,
  ComboboxList,
  ComboboxOption,
} from '@vercel/geistcn/components';
import { LogoIconVercelSvg } from '@vercel/geistcn-assets/logos';

export function Component(): JSX.Element {
  return (
    <Combobox aria-label="Search" placeholder="Search..." className="w-fit">
      <ComboboxInput />
      <ComboboxList>
        <ComboboxOption value="a" suffix={<LogoIconVercelSvg />}>
          One
        </ComboboxOption>
        <ComboboxOption value="b" suffix={<LogoIconVercelSvg />}>
          Two
        </ComboboxOption>
        <ComboboxOption value="c" suffix={<LogoIconVercelSvg />}>
          Three
        </ComboboxOption>
      </ComboboxList>
    </Combobox>
  );
}
```

## With label

```tsx
import {
  Combobox,
  ComboboxInput,
  ComboboxList,
  ComboboxOption,
} from '@vercel/geistcn/components';
import { useId, type JSX } from 'react';

export function Component(): JSX.Element {
  const id = useId();

  return (
    <div className="space-y-2">
      <label className="text-label-14 text-gray-900" htmlFor={id}>
        Select your country
      </label>
      <Combobox
        aria-label="Select your country"
        id={id}
        placeholder="Search countries…"
      >
        <ComboboxInput />
        <ComboboxList>
          <ComboboxOption value="us">United States</ComboboxOption>
          <ComboboxOption value="ca">Canada</ComboboxOption>
          <ComboboxOption value="uk">United Kingdom</ComboboxOption>
          <ComboboxOption value="de">Germany</ComboboxOption>
          <ComboboxOption value="fr">France</ComboboxOption>
          <ComboboxOption value="jp">Japan</ComboboxOption>
          <ComboboxOption value="au">Australia</ComboboxOption>
          <ComboboxOption value="br">Brazil</ComboboxOption>
        </ComboboxList>
      </Combobox>
    </div>
  );
}
```

## Sizes

```tsx
import {
  Combobox,
  ComboboxInput,
  ComboboxList,
  ComboboxOption,
} from '@vercel/geistcn/components';
import type { JSX } from 'react';

export function Component(): JSX.Element {
  const options = [
    { value: 'a', label: 'One' },
    { value: 'b', label: 'Two' },
    { value: 'c', label: 'Three' },
  ];

  return (
    <div className="flex flex-col md:flex-row items-stretch justify-start gap-4 flex-initial">
      <Combobox aria-label="Search" placeholder="Search..." size="small">
        <ComboboxInput />
        <ComboboxList>
          {options.map((option) => (
            <ComboboxOption key={option.value} value={option.value}>
              {option.label}
            </ComboboxOption>
          ))}
        </ComboboxList>
      </Combobox>

      <Combobox aria-label="Search" placeholder="Search...">
        <ComboboxInput />
        <ComboboxList>
          {options.map((option) => (
            <ComboboxOption key={option.value} value={option.value}>
              {option.label}
            </ComboboxOption>
          ))}
        </ComboboxList>
      </Combobox>

      <Combobox aria-label="Search" placeholder="Search..." size="large">
        <ComboboxInput />
        <ComboboxList>
          {options.map((option) => (
            <ComboboxOption key={option.value} value={option.value}>
              {option.label}
            </ComboboxOption>
          ))}
        </ComboboxList>
      </Combobox>
    </div>
  );
}
```

## Used inside a Modal

It is common to use Combobox inside a Modal. On mobile, the Modal automatically renders a Dialog instead.

```tsx
import {
  Combobox,
  Modal,
  Button,
  Label,
  ComboboxInput,
  ComboboxList,
  ComboboxOption,
  ModalBody,
  ModalHeader,
  ModalTitle,
  ModalSubtitle,
  ModalInset,
  ModalActions,
  ModalAction,
} from '@vercel/geistcn/components';
import { useState, type JSX } from 'react';

export function Component(): JSX.Element {
  const [open, setOpen] = useState(false);

  const options = [
    { value: 'a', label: 'One' },
    { value: 'b', label: 'Two' },
    { value: 'c', label: 'Three' },
  ];

  return (
    <div className="flex flex-col md:flex-row items-stretch justify-start gap-4 flex-initial">
      <Button onClick={() => setOpen(true)} size="small">
        Open Modal
      </Button>

      <Modal active={open} onClickOutside={() => setOpen(false)}>
        <ModalBody>
          <ModalHeader>
            <ModalTitle>Create Token</ModalTitle>
            <ModalSubtitle>
              Enter a unique name for your token to differentiate it from other
              tokens and then select the scope.
            </ModalSubtitle>
          </ModalHeader>

          <ModalInset last>
            <div className="flex flex-col items-stretch justify-start gap-2.5 flex-initial">
              <Label value="Region">
                <Combobox
                  aria-label="Region"
                  placeholder="Search..."
                  size="small"
                >
                  <ComboboxInput />
                  <ComboboxList>
                    {options.map((option) => (
                      <ComboboxOption key={option.value} value={option.value}>
                        {option.label}
                      </ComboboxOption>
                    ))}
                  </ComboboxList>
                </Combobox>
              </Label>
              <p className="text-copy-13 text-gray-900">
                This is the region where your database reads and writes will
                take place.
              </p>
            </div>
          </ModalInset>
        </ModalBody>

        <ModalActions>
          <ModalAction onClick={() => setOpen(false)} variant="secondary">
            Cancel
          </ModalAction>

          <ModalAction onClick={() => setOpen(false)}>Submit</ModalAction>
        </ModalActions>
      </Modal>
    </div>
  );
}
```

## Best Practices

### When to use

* Pick `<Combobox>` when users type to filter a known list of values (regions, frameworks, env-var names).
* For short, fixed lists where typing adds nothing, switch to `Select`.
* When more than one value can be chosen at once, use `MultiSelect`.
* For free-form filter strings that don’t resolve to a single option, drop in `Input` with the `search` variant.

### Behavior

* Show a loading state for async results; don’t collapse the list while the request is in flight.
* Render a custom empty state of the form `No {items} match "{query}"` rather than a bare `No results`.
* Inside a `Modal`, Geist switches to a Dialog on mobile automatically; don’t layer a second portal.
* Keep arrow-key navigation through `<Combobox.Option>` items; don’t hijack Enter to submit the surrounding form while the list is open.

### Content

* Visible label is a short Title Case noun (`Region`, `Environment Variable Name`).
* Placeholder is the inline hint: `Search regions`, `DATABASE_URL`. Never bare `Search…` and never the label restated.
* Option text is Title Case for short values and matches canonical branding (`Next.js`, not `NextJS`); same register across the list.
* Validation names the field and constraint, sentence case with a period (`Select a region.`).

### Accessibility

* `<Combobox>` has no `label` prop; pair a sibling `<Label htmlFor>` with the root `id`, or set `aria-label` on the root for icon-only triggers.
* The root accepts `aria-label` only; there is no `aria-labelledby` prop, so the sibling-label path is the way to reference visible text.
* Trap focus inside the popover when nested in a `Modal` so Tab cycles options instead of the page behind it.
