Combobox
A field that is typed in to narrow a list, and a choice made from the list: one value, or many as tags.
Try it
Choosing a combobox
A Select is for a list that can be read: a dozen services, five locations. A combobox is for one nobody wants to scroll — patients, staff, countries — where the reader already knows a piece of the name. It costs a field to type in, so under ten options or so it is the wrong trade.
The value is always from the list. What is typed is a way to an option and not an answer: leave the field with zzz in it and the last choice comes back; leave it empty and the choice is taken away. Free text with suggestions under it is another pattern, and is an Input.
What was typed, in the label
An option stays if its label holds what was typed anywhere in it, whatever the case or the accents, so and finds Sandra and lea finds Léa. Anywhere, and not only at the start, asks the eye why a row is there: the part that matched is in a heavier weight. Weight, not colour — the accent is the check’s. The first option that can be chosen is active as soon as something is typed, so Enter takes the best match. Nothing is completed in the field: it suggests and never completes, the rule of the site’s own search, because a field that types for you is a field that types the wrong thing.
Many
With multiple each choice is a tag in the field, which grows with them; every option has the Checkbox’s box before it; and selectAllLabel adds a first row that is checked, empty, or mixed. That row is hidden while something is typed, where “all” would not mean the ones in view. The list stays open after a choice, and what was typed is cleared, since it has done its work.
The drawn box is filled with the inverse surface; this one is the accent, because it is the Checkbox’s own and one checkbox is enough for a system. It is drawn again here as a picture and is not the component: an input inside an option is a control inside a control.
Anatomy
| Part | Value |
|---|---|
| Field | Input’s box: 32, 40 or 48 tall, radius 12; it grows with its tags |
| Tag | 24 tall, a capsule on the raised surface, 12 / 16 |
| A tag’s button | 20 |
| Clear | 24, the least a target may be |
| Option | the Select’s: 40 tall, radius 6 |
| Checkbox | 20, the Checkbox’s own box |
| Most | eight options, then it scrolls |
The tag is private to the Combobox for now. The system’s Tag is on the roadmap and not yet drawn; when it is, this becomes that.
Accessibility
The field is a text input with role="combobox" and aria-autocomplete="list" that owns a listbox; the focus never leaves it, and the active option is named to it. With many the listbox says it takes many, a change says how many are chosen, and the row for all says mixed. A tag’s button is reached by the pointer and not by Tab — six tags would be six stops before the field — because the keyboard has two other ways: Backspace takes the last away, and the list unchecks any. The Enter that commits an IME composition is the composition’s and chooses nothing.
| Key | Does |
|---|---|
| Typing | Opens the list, narrows it, and makes the best match active. |
| Down, Up | Open the list, and move over the options past what cannot be chosen. |
| Alt + Down | Opens the list and makes nothing active. |
| Home, End | The caret’s, until an option is active; then the first and the last. |
| Enter | Chooses the active option. With many, the list stays open. |
| Esc | Closes the list. Again, puts back what was typed over. Neither goes on to a Dialog around it. |
| Backspace | With many and nothing typed, takes the last tag away. |
| Leaving the field | What was typed and chose nothing is put back: the value is always from the list. Emptied, a single choice is taken away. |
Props
import { Combobox, Avatar, Field } from 'alpenglow';
// One value.
<Field label="Patient">
<Combobox
name="patient"
placeholder="Name or record number"
options={patients}
value={patient}
onChange={setPatient}
emptyText={(q) => `No patients match “${q}”`}
/>
</Field>
// Many: tags in the field, a checkbox before every option.
<Combobox multiple selectAllLabel="All" options={people} value={team} onChange={setTeam} />
// A server does the narrowing.
<Combobox filter={null} onInputChange={search} loading={pending} options={results} … />| Prop | Type | Default |
|---|---|---|
options | Select | required |
multiple | boolean | false |
value, defaultValue | string — or string[] with multiple | — |
onChange | (value) => void | — |
placeholder | string | — |
filter | (option, query) => boolean, or null | the label holds it |
onInputChange | (query: string) => void | — |
loading, loadingText | boolean, string | false, 'Loading…' |
emptyText | string | (query) => string | 'No results' |
clearable, clearLabel | boolean, string | with multiple, 'Clear' |
selectAllLabel | string, with multiple | — |
removeLabel, countLabel | (label) => string, (count) => string | English |
size, invalid, disabled, required, name, iconStart | as the Select | — |
id, aria-label, aria-labelledby, aria-describedby, className | string | from Field |