react ~8 KB 0 deps v0.1.0 ↗ GitHub ↗

@mshafiqyajid/react-combobox

Headless combobox hook and styled component. Type-to-filter, async loadOptions with debounce, grouped options, creatable, clearable, full keyboard navigation, portal dropdown, ARIA combobox pattern.

Playground #

Props
TSX
import { ComboboxStyled } from "@mshafiqyajid/react-combobox/styled";
import "@mshafiqyajid/react-combobox/styles.css";

<ComboboxStyled
  options={options}
  value={value}
  onChange={setValue}
/>

Grouped options

Grouped options (countries by region)

Async loadOptions

Async loadOptions (mock 500ms delay)

Creatable

Creatable — type a new option and press Enter

Form integration (label / hint / error)

Choose your primary framework
This field is required

Install #

npm install @mshafiqyajid/react-combobox

Quick start #

import { useState } from "react";
import { ComboboxStyled } from "@mshafiqyajid/react-combobox/styled";
import "@mshafiqyajid/react-combobox/styles.css";

const options = [
  { value: "react",  label: "React" },
  { value: "vue",    label: "Vue" },
  { value: "svelte", label: "Svelte" },
];

function App() {
  const [value, setValue] = useState<string | null>(null);
  return (
    <ComboboxStyled
      options={options}
      value={value}
      onChange={setValue}
      placeholder="Search frameworks…"
      clearable
    />
  );
}

Async loadOptions #

<ComboboxStyled
  value={value}
  onChange={setValue}
  loadOptions={async (query) => {
    const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
    return res.json(); // ComboboxOption[]
  }}
  debounceMs={300}
  loadingText="Searching…"
  emptyText="No results"
  errorText="Couldn't load — try again"
/>

Debounced on input query; stale requests cancelled via AbortController. Hook exposes isLoading and loadError; listbox sets aria-busy while in flight.

Grouped options #

const options = [
  { value: "react",  label: "React",  group: "Frontend" },
  { value: "vue",    label: "Vue",    group: "Frontend" },
  { value: "django", label: "Django", group: "Backend" },
  { value: "rails",  label: "Rails",  group: "Backend" },
];

<ComboboxStyled options={options} value={value} onChange={setValue} />

Creatable #

<ComboboxStyled
  options={options}
  value={value}
  onChange={setValue}
  creatable
  createLabel={(q) => `Add "${q}"`}
  onCreateOption={(newValue) => {
    // add to your options list and set value
    setOptions((prev) => [...prev, { value: newValue, label: newValue }]);
    setValue(newValue);
  }}
/>

Form fields #

<ComboboxStyled
  options={options}
  value={value}
  onChange={setValue}
  label="Framework"
  hint="Choose your primary framework"
  placeholder="Search…"
  required
/>

{/* With validation error */}
<ComboboxStyled
  options={options}
  value={value}
  onChange={setValue}
  label="Framework"
  invalid
  error="Please select a framework"
/>

Headless #

import { useCombobox } from "@mshafiqyajid/react-combobox";

const options = [
  { value: "react", label: "React" },
  { value: "vue",   label: "Vue" },
];

const {
  inputProps,
  listboxProps,
  getOptionProps,
  isOpen,
  filteredOptions,
  selectedOption,
  clearSelection,
} = useCombobox({ options, value, onChange });

return (
  <div style={{ position: "relative" }}>
    <input {...inputProps} placeholder="Search…" />
    {isOpen && (
      <ul {...listboxProps} className="listbox">
        {filteredOptions.map((opt, i) => (
          <li key={opt.value} {...getOptionProps(opt, i)}>
            {opt.label}
          </li>
        ))}
      </ul>
    )}
  </div>
);

API #

PropTypeDefaultDescription
valuestring | nullControlled selected value
defaultValuestringUncontrolled initial value
onChange(v: string | null) => voidCalled when selection changes
optionsComboboxOption[][]Static options. Each: { value, label, disabled?, group? }
loadOptions(q: string) => Promise<ComboboxOption[]>Async option loader (debounced, cancellable)
debounceMsnumber300Debounce delay for loadOptions
placeholderstring"Search…"Input placeholder
emptyTextstring"No options"Message when no options match
loadingTextstring"Loading…"Message while loading async options
errorTextstringMessage on load error
creatablebooleanfalseShow a "Create" option for unmatched queries
createLabel(q: string) => stringCustom label for the create option
onCreateOption(v: string) => voidCalled when user confirms creating a new option
clearablebooleantrueShow clear button when a value is selected
size"sm" | "md" | "lg""md"Control size (32 / 40 / 48 px)
tone"neutral" | "primary" | "success" | "danger""neutral"Color tone applied to focus ring and selected state
disabledbooleanfalseDisable the combobox
readOnlybooleanfalseInput is read-only
requiredbooleanfalseMarks the field as required
invalidbooleanfalseShow error state styling
labelstringLabel text above the control
hintstringHint text below the control
errorstringError message below the control
placement"auto" | "top" | "bottom""auto"Dropdown side. "auto" picks bottom unless there isn't room.
offsetnumber4Gap (px) between control and dropdown
flipbooleantrueAuto-flip when there isn't room on the chosen side
renderOption(opt, state) => ReactNodeCustom option renderer

Keyboard #

KeyAction
TypeFilter options by query
ArrowDown / ArrowUpNavigate options
EnterSelect focused option (or trigger create)
EscapeClose dropdown / clear query
Backspace (empty)Clear current selection
TabClose dropdown
Edit this page on GitHub