Combobox

Developer Experience (DX)

How developers use the Combobox, with the Select's typed list of options, opt-in props for typed values and a slot for each part.

Next, let's look at how developers use the Combobox. You give it the same list of options a Select takes and a name, put it in a Field, and bind its value only when your code needs to read it.

import { useState } from "react";
import { Combobox } from "@/components/Combobox";
import { Field } from "@/components/Field";

const countries = [
  { value: "fr", label: "France" },
  { value: "es", label: "Spain" },
  { value: "nz", label: "New Zealand" },
];

export function Shipping() {
  const [country, setCountry] = useState<string>();

  return (
    <Field label="Country">
      <Combobox id="country" name="country" options={countries} placeholder="Type a country" value={country} onChange={(event) => setCountry(event.target.value)} />
    </Field>
  );
}

Consistent

The Combobox takes the Select's names wherever the two do the same thing, so moving a field from one to the other is a change of component, not of props: options, multiple, placeholder, the same size scale as the Input, disabled, readonly, required, removeLabel and removedLabel, and the Select's slots, icon included. Like the Select, classes and styles go on the root, name and form on the hidden <select>, where change and input fire, and id, ARIA and autocomplete on the text field, since that's what a label and autofill point at.

NameKindWhat it means
optionsPropThe Select's list of options, groups and separators
multiplePropPicks several values, each a tag, as on the Select
sizePropxs, sm, md, lg or xl, as on the Input and the Select
disabledPropIt can't be used, and leaves its form
readonlyPropThe value shows and submits, but can't change
requiredPropThe form needs a value, which the Field sets
placeholderPropThe text shown in the empty field
namePropThe name the value submits under
changeEventThe value changed

The names the Select doesn't have are the ones for what only typing brings: allowCustomValue and creatable for typed values, filter for how options match, and emptyLabel, resultsLabel, createLabel and toggleLabel for the text it shows and announces. There's no nativeOnTouch, since a native picker can't take typed text, and no value slot or countLabel, since the text field shows the picked option's label, which people edit as they type, and the tags show several values.

Type-safe

The props, the options, the value and every slot are typed, so your editor can autocomplete them and your type checker catches mistakes like size="medium". The options are the Select's own types, so one list feeds both components.

PropTypeDefaultDescription
optionsSelectItem[]Required. The options, groups and separators people pick from
valuestring | string[]The value, bound or not, as an array with multiple
multiplebooleanfalsePicks any number of values, each shown as a tag
placeholderstringShown in the empty field, and never a value
size"xs" | "sm" | "md" | "lg" | "xl""md"How big the Combobox is
disabledbooleanfalseDisables the text field and the <select>, and leaves its form
readonlybooleanfalseKeeps the value in its form, and doesn't open
requiredbooleanfalseAsks for a value, and the Field sets it
allowCustomValuebooleanfalseKeeps typed text as the value, for one value only
creatablebooleanfalseEnds the list with an option that adds what people typed
createLabel(text: string) => string`Create "${text}"`The create option's label
filter(option: SelectOption, text: string) => booleanAny part of the label, ignoring case and accentsWhether an option matches what people typed
emptyLabelstring"No results"Shown and announced when nothing matches
resultsLabel(count: number) => string`${count} results`, or "1 result"Announced once typing pauses
toggleLabelstring"Suggestions"The toggle button's name
removeLabel(label: string) => string`Remove ${label}`A tag's remove button's name
removedLabel(label: string) => string`${label} removed`What screen readers hear after a removal
EventPayloadDescription
createstringPeople picked the create option, with the text they typed
changeEventFires from the <select>, and bubbles, once the value changes, never on every key
inputEventFires from the <select>, and bubbles, right before change
SlotGetsDescription
leadingAn icon before the text, hidden from assistive technologies
arrowThe chevron in the toggle
optionoption, selected, activeHow an option reads. The check mark stays beside it
tagoptionHow a picked value reads in its tag. The remove button stays
group-labellabelHow a group's heading reads
separatorThe rule between options or groups
iconoptionAn option's icon, in the panel, the field and its tag

React and Solid take groupLabel for the group-label slot and onCreate for the event, and Angular binds (create), each in its own idiom. Import the types from the Select, where they live, so a list typed for one works for both:

import type { SelectItem } from "@/components/Select";
import type { ComboboxProps } from "@/components/Combobox";

const countries: SelectItem[] = [
  { value: "fr", label: "France" },
  { value: "nz", label: "New Zealand" },
];

Composable

Put the Combobox in a Field, and it gets its label, hint, error, required, disabled and size without any wiring. It takes the same text-control path through the Field as an Input and a Select do.

The slots let you change what's inside each part without giving up how it behaves. The option slot keeps the check mark beside what you render, and the tag slot keeps the remove button. Here's an option that shows the country's code beside its name:

<Combobox id="country" name="country" options={countries}
  leading={<GlobeIcon />}
  option={({ option, selected }) => <>{option.label} <small>{option.value.toUpperCase()}</small>{selected && " (yours)"}</>}
/>

Controllable

Bind the value when your code reads it, and leave it unbound when the form is all you need. The <select> holds it either way, so an unbound Combobox still submits and resets.

// Controlled: your state decides.
<Combobox id="country" name="country" options={countries} value={country} onChange={(event) => setCountry(event.target.value)} />
// Uncontrolled: the Combobox keeps its own, and its form submits it.
<Combobox id="country" name="country" options={countries} defaultValue="fr" />
// New values: add them to your list as people create them.
<Combobox id="topics" name="topics" multiple creatable options={topics} onCreate={(text) => setTopics([...topics, { value: text, label: text }])} />

change and input fire from the <select> when the value changes, not on every key, so a listener on the Combobox or on its form hears picks, typed values once they're kept and removals, with the value already updated. Listen for create to add a new value to your list, so people find it again next time. A ref gets you the text field and the <select>, through control and select, for focusing it from your own code.

DX Checklist

RuleLevelScopeRequirementBasisCheck
combobox/select-namesShouldComponentIt takes the Select's names for what the two share: options, multiple, size, placeholder, disabled, readonly, required and the slotsOpen ComponentsReview
combobox/typedMustComponentIts props, events and slots are typed, and it takes the Select's exported option typesOpen ComponentsType check
combobox/valueMustComponentThe value is a string, or an array with multiple, bound or unbound from a value attribute, and the text follows a bound value as it changesOpen ComponentsUnit test
combobox/native-eventsShouldComponentchange and input fire from its <select> when the value changes, never on every keyHTMLUnit test
combobox/attributesMustComponentClasses and styles go on the root, name and form on the <select>, and id, ARIA and autocomplete on the text fieldOpen ComponentsUnit test
combobox/in-a-fieldMustBothIn a Field, it takes the label, description, invalid and disabled stateWCAG 1.3.1 (A)Unit test
combobox/slotsShouldComponentEach part has a slot or tokens to restyle it, and the check mark and remove button stayOpen ComponentsUnit test
combobox/create-eventMustComponentPicking the create option fires create with the text people typedOpen ComponentsUnit test
combobox/custom-filterShouldComponentA filter prop replaces how options match what people typedOpen ComponentsUnit test
combobox/translatableShouldComponentEvery string it shows or announces comes through a propOpen ComponentsUnit test
combobox/exposedShouldComponentA ref gets the text field and the <select>Open ComponentsUnit test

FAQ

Copyright © 2026