---
id: "packages/active-ui/search-combobox"
title: "SearchCombobox"
description: "A searchable choice from a list your application already has, with a search field in the menu and the option's value as identity."
url: "/packages/active-ui/search-combobox"
locale: "en"
revision: "4444dc9164ed2375"
---

# SearchCombobox

Use `SearchCombobox` when the reader has to search a known list to choose one item: a scheme, a
category, a role. The button shows the choice; the menu opens with a search field on top and a list
that filters while typing. For a short fixed list where searching does not help, use
[Select](/packages/active-ui/select); when the server has to search, use
[AsyncCombobox](/packages/active-ui/async-combobox).

**Example: A searchable choice from a local list**

Client: Runs entirely in your browser. Source: `examples/ui/search-combobox.tsx`.

```tsx
// apps/docs/examples/ui/search-combobox.tsx
'use client';

import {useState} from 'react';

import {SearchCombobox} from '@workspace/active-ui/components/composed/search-combobox';
import {
  Field,
  FieldDescription,
  FieldLabel,
} from '@workspace/active-ui/components/primitives/field';

// A local list: the component filters it. `keywords` is searched, not shown.
const SCHEMES = [
  {value: 'dc-2024', label: 'Defined contribution 2024', keywords: 'DC'},
  {value: 'db-final', label: 'Final salary', keywords: 'DB'},
  {value: 'db-average', label: 'Career average', keywords: 'DB CA'},
  {value: 'dc-2019', label: 'Defined contribution 2019', keywords: 'DC'},
  {value: 'anw', label: 'Surviving dependants', keywords: 'ANW gap'},
  {value: 'closed', label: 'Early retirement (closed)', disabled: true},
];

/**
 * A searchable choice from a local list. Type on the focused button to
 * start searching; the search ignores case and accents and also matches the
 * hidden codes (try "DB"). The value is the option's `value`, never its label.
 */
export default function SearchComboboxExample() {
  const [scheme, setScheme] = useState<string | null>('db-average');
  return (
    <Field className="w-full max-w-xs">
      <FieldLabel htmlFor="scheme">Pension scheme</FieldLabel>
      <SearchCombobox
        id="scheme"
        options={SCHEMES}
        value={scheme}
        onValueChange={setScheme}
        placeholder="Choose a scheme"
        searchLabel="Search schemes"
        searchPlaceholder="Search…"
        emptyText="No scheme found."
        locale="en"
        aria-describedby="scheme-value"
      />
      <FieldDescription id="scheme-value">
        Value: <code>{scheme ?? 'null'}</code>
      </FieldDescription>
    </Field>
  );
}
```

## What the composition adds

`SearchCombobox` is the [Combobox](/packages/active-ui/combobox) primitive with a button as trigger
and the search field inside the menu. It adds:

- **identity by `value`**: two options with the same label stay distinct, and changing the language
  does not change the stored value;
- **filtering per language**: case- and accent-insensitive, anywhere in the label, with the
  collator of the `locale` you pass. Under `es`, `Espana` does not find `España`; under `en` it
  does;
- **`keywords`**: extra text that is searched but not shown, such as a code;
- **typing on the button starts a search**: a key typed on the focused, closed button opens the menu
  and becomes the start of the query, instead of choosing the first matching option;
- **`readOnly`** shows the choice but does not open; `required` marks the button for assistive
  technology.

No groups, no clear button, no multiple choice ([SearchMultiCombobox](/packages/active-ui/search-multi-combobox)),
no result limit or virtualisation, and no loading state.

## Usage

```tsx
import {SearchCombobox} from '@workspace/active-ui/components/composed/search-combobox';

<SearchCombobox
  id="scheme"
  options={[{value: 'db-final', label: 'Final salary', keywords: 'DB'}]}
  value={scheme} // the option's value, or null
  onValueChange={setScheme}
  placeholder="Choose a scheme"
  searchLabel="Search schemes"
  searchPlaceholder="Search…"
  emptyText="No scheme found."
  locale="en"
/>;
```

## What your application provides

- The complete list, with stable values and translated labels; the component filters it, so do not
  filter it as well.
- The reader's language as `locale`, every text translated, and the form binding.
- What `null` means for the field, and the validation.

## Accessibility and keyboard

- The button is the field: name it with `FieldLabel htmlFor` its `id`; `aria-describedby` and
  `aria-invalid` go to the button.
- `Enter`, `Space` or `ArrowDown` open the menu; the search field is named by `searchLabel`.
- In the menu, `ArrowUp` and `ArrowDown` move through the hits, `Enter` chooses, `Escape` closes
  and returns focus to the button.

## API

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `emptyText` | `string` | yes | Text when the search returns nothing. Supplied translated. |
| `locale` | `string` | yes | The reader's language, for matching the query against labels.  Required, because matching is a language question: under `es` the query `Espana` does not find `España` (ñ is its own letter), under `en` it does. Without it Base UI's collator falls back to the runtime locale, which need not be the reader's language. Pass the locale the page already resolved. |
| `onValueChange` | `(value: string \| null) => void` | yes | Receives the new `value`, or `null`.  `null` is part of the contract because an empty field is a valid state; what it means (uncategorised, no choice, validation error) is the caller's call. |
| `options` | `readonly SearchComboboxOption[]` | yes | The complete, locally available option list. The component filters itself; the caller does not filter as well.  This component is not meant for options that must be searched server-side; see the notes on {@link SearchCombobox}. |
| `placeholder` | `string` | yes | Text on the button while nothing is selected. Supplied translated. |
| `searchLabel` | `string` | yes | Accessible name of the search field inside the menu. Supplied translated.  Required: the field has no visible label, and its placeholder is not a name. The caller owns the wording, so two pickers on one page can be told apart ("Search category", "Search participant"). |
| `searchPlaceholder` | `string` | yes | Text in the search field inside the menu. Supplied translated. |
| `value` | `string \| null` | yes | The selected `value`, or `null` when nothing is selected. |
| `readOnly` | `boolean` | no | Shows the choice but ignores edits: the menu does not open and the value cannot change, while the button stays focusable and readable (unlike `disabled`, which takes it out of the tab order's meaning and greys it). |
| `required` | `boolean` | no | Marks the choice as required for assistive technology. Validation itself stays with the caller's schema; this component does not block anything. |

**SearchComboboxOption**

```ts
interface SearchComboboxOption {
  value: string;
  label: string;
  disabled?: boolean;
  keywords?: string;
}
```

**SearchComboboxProps**

```ts
interface SearchComboboxProps extends Omit<
  React.ComponentProps<'button'>,
  'value' | 'defaultValue' | 'onChange' | 'children'
> {
  options: readonly SearchComboboxOption[];
  value: string | null;
  onValueChange: (value: string | null) => void;
  placeholder: string;
  searchLabel: string;
  searchPlaceholder: string;
  emptyText: string;
  locale: LocaleCode;
  readOnly?: boolean;
  required?: boolean;
}
```
