# Combobox (`combobox`) - SignalOS widget

> Searchable single-select combobox with async search, infinite loading, clearable selection, and custom option rendering.

- **Version:** 0.4.0
- **Kind:** widget · **Category:** inputs
- **Install:** `npx shadcn@latest add @signalos/combobox`
- **Registry dependencies (pulled automatically):** @signalos/tokens, @signalos/utils, @signalos/button, @signalos/popover, @signalos/command
- **npm dependencies:** lucide-react@^1.7.0, class-variance-authority@^0.7.1
- **Files installed:** `src/components/widgets/combobox/Combobox.tsx`, `src/components/widgets/combobox/Combobox.types.ts`, `src/components/widgets/combobox/Combobox.constants.ts`, `src/components/widgets/combobox/Combobox.utils.ts`

## Access

This is a private registry: pulling source requires a `SIGNALOS_REGISTRY_TOKEN`
(GitHub fine-grained PAT with read access to the signal-widgets repo) and an
`@signalos` entry in components.json `"registries"`. Previews and this document are public.

## Usage

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

## Example

```tsx
// Example: async search with a clearable selection.
import { useState } from "react"

import { Combobox } from "@/components/widgets/combobox/Combobox"
import type { ComboboxOption } from "@/components/widgets/combobox/Combobox.types"

const countries: ComboboxOption[] = [
  { value: "in", label: "India" },
  { value: "us", label: "United States" },
  { value: "de", label: "Germany" },
]

export default function Example() {
  const [country, setCountry] = useState("")
  const [options, setOptions] = useState(countries)

  return (
    <Combobox
      value={country}
      options={options}
      clearable
      onSearch={(query) =>
        setOptions(
          countries.filter((option) =>
            option.label?.toString().toLowerCase().includes(query.toLowerCase())
          )
        )
      }
      onSelect={setCountry}
    />
  )
}
```

## Props

### `ComboboxOption`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `value` | `string` | yes | - |  |
| `label` | `ReactNode` | yes | - |  |
| `icon` | `ReactNode` | no | - |  |
| `disabled` | `boolean \| undefined` | no | - |  |
| `keywords` | `string[] \| undefined` | no | - |  |

### `ComboboxSize`

```ts
export type ComboboxSize = "sm" | "md" | "lg"
```

### `ComboboxProps`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `options` | `ComboboxOption[]` | yes | - |  |
| `value` | `string \| undefined` | no | - |  |
| `onSearch` | `((query: string) => void) \| undefined` | no | - |  |
| `onSelect` | `(value: string) => void` | yes | - |  |
| `loading` | `boolean \| undefined` | no | - |  |
| `disabled` | `boolean \| undefined` | no | - |  |
| `clearable` | `boolean \| undefined` | no | - |  |
| `onClear` | `(() => void) \| undefined` | no | - |  |
| `hasMore` | `boolean \| undefined` | no | - |  |
| `onLoadMore` | `(() => void) \| undefined` | no | - |  |
| `placeholder` | `string \| undefined` | no | - |  |
| `ariaLabel` | `string \| undefined` | no | - | Accessible name for the trigger. Defaults to `placeholder`.  The trigger carries `role="combobox"`, and the accessible-name algorithm does not credit that role with a name from its visible text content the way it would a plain button — so without this the control is announced as unlabelled even though sighted users can see the selected value. Set this explicitly when the placeholder is not a meaningful label on its own (e.g. "Select…"). |
| `emptyMessage` | `ReactNode` | no | - |  |
| `loadMoreLabel` | `ReactNode` | no | - |  |
| `renderOption` | `((option: ComboboxOption) => ReactNode) \| undefined` | no | - |  |
| `size` | `ComboboxSize \| undefined` | no | - |  |
| `className` | `string \| undefined` | no | - |  |
| `contentClassName` | `string \| undefined` | no | - |  |
| `data-testid` | `string \| undefined` | no | - | Test identifier rendered as `data-testid` on the root element. |

## Changelog

# combobox

## 0.4.0

- The trigger now carries an accessible name: `aria-label` defaults to
  `placeholder`, with a new optional `ariaLabel` prop to override it. The
  trigger has `role="combobox"`, and the accessible-name algorithm does not
  credit that role with a name from its visible text the way it would a plain
  button — so screen readers previously announced the control as unlabelled.
  The axe assertion in the test suite no longer needs its `button-name`
  override.

## 0.3.0

- The clearable inline "X" button now has `aria-label="Clear selection"` and a
  `${dataTestId}-clear` test id, matching every other interactive element in
  the widget.

## 0.2.0

- **Breaking:** rename the `testId` prop to `data-testid` to match the rest
  of the registry and the DOM attribute name.

## 0.1.1

- Content sync fix to bring source in line with published registry payload.


## 0.1.0

- Initial release: single-select combobox on shadcn/ui `Popover` + `Command`,
  with async search, infinite loading ("Load more"), clearable selection,
  disabled options, and custom option rendering.
