---
id: "packages/active-ui/search-multi-combobox"
title: "SearchMultiCombobox"
description: "A searchable multiple choice from a list your application already has, summarised on one line in the button."
url: "/packages/active-ui/search-multi-combobox"
locale: "en"
revision: "07d38a9009dba446"
---

# SearchMultiCombobox

Use `SearchMultiCombobox` when the reader picks several items from a list that is easier to search
than to scan: events, languages, roles as a field in a form. It is the multiple form of
[SearchCombobox](/packages/active-ui/search-combobox), with the same identity and search. When
the reader should weigh a small closed set together, use checkboxes instead.

**Example: Several events from a searchable list**

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

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

import {useState} from 'react';

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

const EVENTS = [
  {
    value: 'participant.created',
    label: 'Participant added',
    keywords: 'participant.created',
  },
  {
    value: 'participant.updated',
    label: 'Participant changed',
    keywords: 'participant.updated',
  },
  {
    value: 'statement.sent',
    label: 'Statement sent',
    keywords: 'statement.sent',
  },
  {
    value: 'statement.failed',
    label: 'Statement failed',
    keywords: 'statement.failed',
  },
  {
    value: 'import.finished',
    label: 'Import finished',
    keywords: 'import.finished',
  },
];

/**
 * A searchable multiple choice. The menu stays open while you choose; the
 * closed button shows up to two labels and then the count. The technical
 * event keys are searchable but not shown. What an empty choice means is the
 * application's decision.
 */
export default function SearchMultiComboboxExample() {
  const [events, setEvents] = useState<string[]>(['statement.sent']);
  return (
    <Field className="w-full max-w-xs">
      <FieldLabel htmlFor="webhook-events">Send a webhook on</FieldLabel>
      <SearchMultiCombobox
        id="webhook-events"
        options={EVENTS}
        value={events}
        onValueChange={setEvents}
        placeholder="Choose events"
        searchLabel="Search events"
        searchPlaceholder="Search…"
        emptyText="No event found."
        summaryText={(count) => (count === 1 ? '1 event' : `${count} events`)}
        clearLabel="Clear the choice"
        locale="en"
        aria-describedby="webhook-events-value"
      />
      <FieldDescription id="webhook-events-value">
        Value: <code>{JSON.stringify(events)}</code>
      </FieldDescription>
    </Field>
  );
}
```

## What the composition adds

- **One line in the button.** Up to two labels, then your `summaryText` with the count. The button
  never wraps.
- **The menu stays open while choosing**, with the search field on top and a check mark on each
  chosen option, and the count at the bottom.
- **A stable order**: chosen values keep their order, a new value is added at the end.
- **`clearLabel`** adds a clear action; without it there is none. Clearing empties the choice, it
  never means "all".
- **Unknown values are kept.** A stored value with no option in the list stays in the value and in
  the count; the component cannot show a label for it.

No chips, no "select all", no groups or tree, no free values and no server search.

## Usage

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

<SearchMultiCombobox
  id="events"
  options={events}
  value={chosen} // string[]
  onValueChange={setChosen}
  placeholder="Choose events"
  searchLabel="Search events"
  searchPlaceholder="Search…"
  emptyText="No event found."
  summaryText={(count) => `${count} events`}
  clearLabel="Clear the choice"
  locale="en"
/>;
```

## What your application provides

- The list, `summaryText` as a function so your translation can handle the plural, and the other
  texts.
- What an empty choice means (none, all, inherit) and whether it is allowed.

## Accessibility and keyboard

- Name the button with `FieldLabel htmlFor` its `id`.
- In the menu, `ArrowUp` and `ArrowDown` move, `Enter` toggles the active option, and `Escape`
  closes.

## 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 for the same reason as on `SearchCombobox`: without it Base UI's collator uses the runtime locale. |
| `onValueChange` | `(value: string[]) => void` | yes | Receives the new set of identities.  **The order is stable and documented**: existing values keep their relative position, a new value is appended. That is not a domain ordering (membership is unordered), but it prevents a `value` from changing shape on every click. |
| `options` | `readonly SearchMultiComboboxOption[]` | yes | The complete, locally available option list. The component filters itself; the caller does not filter as well.  Server-side search does not exist for this component: that is a different lifecycle (loading, failing, stale responses) and there is no consumer for a multiple remote variant today. |
| `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. |
| `summaryText` | `(count: number) => string` | yes | The summary once the button no longer spells out the labels, and the line at the bottom of the menu.  A function and not a string, because it contains a number and `active-ui` neither interpolates nor translates: `(n) => t('common.selection.count', {count: n})`. |
| `value` | `readonly string[]` | yes | The selected identities. Empty is a valid state.  **What empty means is the caller's call** and never this component's: none, all, inherit or no filter are four different domain meanings. |
| `clearLabel` | `string` | no | The label of the clear action. **If it is missing, there is no clear action.**  Clearing means only: the explicit set is empty. It never switches to "all", and whether empty is allowed is decided by the caller's validation. |

**SearchMultiComboboxOption**

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

**SearchMultiComboboxProps**

```ts
interface SearchMultiComboboxProps extends Omit<
  React.ComponentProps<'button'>,
  'value' | 'defaultValue' | 'onChange' | 'children'
> {
  options: readonly SearchMultiComboboxOption[];
  value: readonly string[];
  onValueChange: (value: string[]) => void;
  placeholder: string;
  searchLabel: string;
  searchPlaceholder: string;
  emptyText: string;
  summaryText: (count: number) => string;
  clearLabel?: string;
  locale: LocaleCode;
}
```
