Skip to content
WANCRAFT

F.113Forms & Inputs

Combobox

An ARIA 1.2 autocomplete with match highlighting, groups, async search, multi-select chips and create-on-type.

NewReactMotionTailwindadvanced

Overview

Combobox is an editable autocomplete built on the ARIA 1.2 combobox pattern: the input owns a listbox through aria-controls and aria-expanded and follows the highlighted option with aria-activedescendant, so focus never leaves the text field. Static options are filtered with the matched text underlined and can be grouped under headings; a loadOptions function switches to debounced async search where every newer query aborts the previous request, with loading, error and empty states. It selects one value or many (as animated chips), can offer a Create option for unknown entries, and is controlled or uncontrolled with label, hint, error and native form values. The list springs open, and a single highlight glides between options as you move with the keyboard or pointer.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/combobox.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/combobox.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/combobox.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { Combobox, type ComboboxOption } from "@/components/ui/combobox";

export function ComboboxExample() {
  const cities: ComboboxOption[] = [
    { value: "kul", label: "Kuala Lumpur", group: "Asia Pacific" },
    { value: "sin", label: "Singapore", group: "Asia Pacific" },
    { value: "tyo", label: "Tokyo", group: "Asia Pacific" },
    { value: "lon", label: "London", group: "Europe" },
    { value: "ber", label: "Berlin", group: "Europe" },
    { value: "nyc", label: "New York", group: "Americas" },
  ];

  return (
    <div className="w-full max-w-sm">
      <Combobox label="Office" options={cities} name="office" />
    </div>
  );
}

03Props

PropTypeDefaultDescription
optionsComboboxOption[]—Static options ({ value, label, group?, description?, disabled? }), filtered by substring.
loadOptions(query: string, signal: AbortSignal) => Promise<ComboboxOption[]>—Async source; replaces options. The signal aborts when a newer query starts or the list closes.
debouncenumber250Delay in ms before loadOptions runs.
multiplebooleanfalseSelect several values, shown as removable chips.
creatablebooleanfalseOffer a Create option when nothing matches the query exactly.
onCreate(label: string) => ComboboxOption | void—Builds the created option; defaults to { value: label, label }.
value / defaultValuestring[]—Controlled selection, or the initial selection when uncontrolled. Single mode holds at most one value.
onChange(values: string[], options: ComboboxOption[]) => void—Called after every selection change.
labelReactNode"Search"Visible <label> for the input.
hint / errorReactNode—Helper text and error message, linked via aria-describedby; error also sets aria-invalid.
placeholderstring"Type to search…"Input placeholder.
emptyMessagestring"No matches"Shown when the query matches nothing.
accentstring"#FF7448"Focus ring, caret, highlight and match colour.
disabledbooleanfalseDisables the field.
namestring—Submits one hidden input per selected value.
inputIdstring—id for the input (generated when omitted).

04Features

  • 01ARIA 1.2 editable combobox: input + listbox with aria-activedescendant, focus stays in the field
  • 02Substring filtering with the matched characters underlined; optional labelled groups
  • 03Async loadOptions with debounce and AbortSignal cancellation; loading rail, error and empty states
  • 04Single select (shows the label, clear button) or multi-select with animated chips
  • 05creatable adds a Create option for unknown entries
  • 06Spring open/close and one highlight that glides between options; list scrolls via scrollTop only
motion · springmotion · layoutinput · keyboardinput · focusinput · click

05Accessibility

  • The input has role="combobox" with aria-expanded, aria-controls, aria-autocomplete="list" and aria-activedescendant; the popup is a role="listbox" (aria-multiselectable in multi mode) with role="group" sections labelled by their headings.
  • aria-selected reflects the chosen value(s); disabled options use aria-disabled and are skipped by the keyboard.
  • ArrowDown/ArrowUp open and move (wrapping), Alt+ArrowDown opens without moving, Home/End and PageUp/PageDown jump while navigating, Enter chooses, Escape closes then clears, Tab accepts the highlighted option in single mode.
  • Result counts, loading and selection changes are announced in a polite live region; hint and error are linked with aria-describedby.
  • prefers-reduced-motion removes the list spring, the gliding highlight and the chip animations.

06Limitations

  • Static filtering is case-insensitive substring matching (no fuzzy or diacritic-insensitive matching); pass loadOptions for custom matching.
  • Chip remove buttons are pointer targets outside the tab order; keyboard users remove chips with Backspace or by toggling the option in the list.