Combobox
StableA search box with suggestions that is never a closed list: whatever is typed can be used.
Also known as autocomplete, typeahead, searchable select, auto complete
Install with npm install @tattva/ui (setup), or copy the code from the kit in Downloads.
Company search with popular names
function Example() {
const [v, setV] = useState("");
return (
<div className="w-72 max-w-full">
<Combobox label="Company" placeholder="Search for a company" value={v} onChange={setV} options={companies} popular={["Amazon", "Flipkart", "Swiggy"]}
onSelect={(val, how) => console.log(how, val)} />
</div>
);
}Focus the empty box to see the popular names and the hint. Type ama and press Enter to pick Amazon.
When to use it
Help people name something quickly, such as a company or a bank, without forcing them to choose from a list that may not contain it.
Made of: Search icon · Input · Popular names or matches · Use typed text row · Empty hint
Use it for
- Asking which company, bank or shop, where the list can never be complete.
- A name field where common answers are one tap and any other answer is still welcome.
- Search where the text itself is the answer, not an id.
Not for
- A fixed set where only the listed values are valid: use Select.
- Running commands or jumping around the product: use CommandPalette.
- A plain text answer with no suggestions: use TextField.
- A list of actions opened from a button: use Menu.
Do and avoid
Do
- Show popular names for the answers most people give.
- Keep the last row that accepts the typed text, so the list never blocks anyone.
- Use the text as the answer. Do not turn it into an id the person cannot see.
- Say what the last row does in the words of your product with useTypedLabel.
Avoid
- Do not use it when only the listed values are allowed. Use a Select.
- Do not show more than a handful of matches. The list stops at 6.
- Do not clear the box when the typed text is not in the list.
- Do not use it to run commands. A command palette has a different job.
On a phone
- The field is 16px on a touch screen, so the page does not zoom when it takes focus.
- The suggestion list opens under the field, shows at most six rows, and each row is 44px tall.
- The list always opens below the field, so near the bottom of a phone the on-screen keyboard can cover it; scroll the field up first.
- Tapping a suggestion does not take focus from the field.
Properties
| Prop | Type | Default | Description |
|---|---|---|---|
| valuerequired | string | none | The text in the box. It is always the text, never an id. |
| onChangerequired | (value: string) => void | none | Called on every keystroke, and with the option's label when a suggestion is picked. |
| optionsrequired | { value: string; label: string; hint?: string }[] | none | The suggestions. Matching uses the label only, ignoring case. hint shows small on the right of the row. |
| popular | string[] | none | Labels shown before typing. A label that matches an option uses that option's value. |
| onSelect | (value: string, how: "option" | "typed") => void | none | Called when a suggestion is picked (how is option, with the option's value) or the typed text is accepted (how is typed, with the text). |
| labelrequired | string | none | Accessible name of the box. |
| placeholder | string | none | Hint text shown while the box is empty. |
| useTypedLabel | (typed: string) => string | Use “typed” | Words for the last row while typing, which keeps exactly what was typed. |
| emptyHint | string | "Not here? Type its name above." | Line shown under the popular names, saying any name can be typed. |
| invalid | boolean | none | Danger border and aria-invalid on the input. Explain the problem in text too. |
| disabled | boolean | none | Dims the box, ignores typing and keeps the list closed. |
| id | string | none | Id of the input. Field sets it. |
| aria-describedby | string | none | Ids of the hint and error. Field sets it. |
| aria-invalid | boolean | none | Marks the input invalid. Field sets it when it has an error. |
| required | boolean | none | Passed to the input. Field sets it. |
| className | string | none | Classes for the outer wrapper. |
"Show code under the preview gives the code for the example on the stage."
In Figma
In Figma, one Combobox switches all of this from its properties panel, across 10 variants. Each property sets the React prop beside it, so design and code use the same names.
| Figma property | React | Values |
|---|---|---|
| Invalid | invalid | On or off |
| State | Hover, Pressed and Focus come from the browser. Disabled sets disabled. | Default, Hover, Pressed, Focus, Disabled |
| Placeholder | placeholder | Any text |
States
States this part has beyond its default look. Build each one your screen can reach. States set by a property are in the Code tab.
- closed
- The list is hidden until the box has focus and there is something to show. Escape closes it again until you type or press an arrow key.
- popular
- Focused and empty, the popular names show with the empty hint under them.
- typing
- Up to 6 matches show, starting-with matches first, followed by a row that keeps exactly what was typed.
- no match
- When nothing matches, only the row that keeps the typed text shows, and the live region says there are no suggestions.
- active option
- The row reached with the arrow keys is shaded, has a bar on its left edge and aria-selected, and is named by aria-activedescendant.
- hover
- A row is shaded when the pointer is over it.
- focus
- A focus ring is drawn around the whole box when the input is reached with the keyboard.
- disabled
- Pass disabled. The box is dimmed and the list never opens.
- invalid
- Pass invalid, or aria-invalid from Field, for a danger border.
Tokens
Change one of these and every part that uses it follows.
--surface-raised--border--fg--fg-muted--surface-hover--shadow-md--focus-ring--danger--radius-overlay
Built in
- The input has role combobox with aria-expanded, aria-controls, aria-autocomplete set to list, and aria-activedescendant naming the active option. Focus stays in the input.
- The list has role listbox. Each row has role option, and the active row has aria-selected.
- Arrow Down and Arrow Up move through the rows and wrap round. Enter picks the active row. Escape closes the list, and pressing it again does nothing.
- With no active row, Enter picks the top suggestion when the typed text is the start of its label. Otherwise it accepts the typed text.
- A polite live region says how many suggestions there are, or that there are none and Enter will use what was typed.
- Clicking or tapping a row picks it without taking focus out of the input.
- Rows are 44px tall on coarse pointers. The active row has a bar on its left edge as well as a shade, and uses the system highlight in forced colours.
What you need to do
- Pass a label. It is the accessible name of the box. Put it inside Field to also show a visible label.
- Spread the props from Field onto the Combobox so the hint and error are linked.
- Keep value in the parent. The text is the answer, so read it from there as well as from onSelect.
- If the answer is invalid, say why in text (Field does this with error) and also set invalid.
Built toward WCAG 2.2 AA and checked automatically; no independent audit yet. Read the accessibility guide