Combobox

v0.4.0

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

View as Markdown

Install

npx shadcn@latest add @signalos/combobox

Requires a configured @signalos registry and a valid SIGNALOS_REGISTRY_TOKEN - get access. Registry dependencies (@signalos/tokens, @signalos/utils, @signalos/button, @signalos/popover, @signalos/command) are pulled automatically.

Preview

Example & code

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

PropTypeDefaultDescription
value*string-
label*ReactNode-
iconReactNode-
disabledboolean | undefined-
keywordsstring[] | undefined-

ComboboxProps

PropTypeDefaultDescription
options*ComboboxOption[]-
valuestring | undefined-
onSearch((query: string) => void) | undefined-
onSelect*(value: string) => void-
loadingboolean | undefined-
disabledboolean | undefined-
clearableboolean | undefined-
onClear(() => void) | undefined-
hasMoreboolean | undefined-
onLoadMore(() => void) | undefined-
placeholderstring | undefined-
ariaLabelstring | undefined-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…").
emptyMessageReactNode-
loadMoreLabelReactNode-
renderOption((option: ComboboxOption) => ReactNode) | undefined-
sizeComboboxSize | undefined-
classNamestring | undefined-
contentClassNamestring | undefined-
data-testidstring | undefined-Test identifier rendered as `data-testid` on the root element.

Supporting types

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

npm dependencies

lucide-react@^1.7.0class-variance-authority@^0.7.1

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.