Combobox
A combobox lets people choose one option from a long list by typing to filter it. The value is always one of your options; typed text only finds it.
<tk-field label="Country">
<tk-combobox name="country" placeholder="Start typing a country">
<option value="dk">Denmark</option>
<option value="de">Germany</option>
<option value="nl">Netherlands</option>
<option value="gb">United Kingdom</option>
</tk-combobox>
</tk-field>Put the combobox in a aria-label. A placeholder is not a name: it disappears as soon as people type.
Anatomy
| Part | What it is |
|---|---|
tk-combobox |
the element; one Tab stop |
| field | a text input: shows the chosen option, or what people type; fills its container |
| clear button | appears while the field has text; clears the text and the value |
| chevron | turns over while the menu is open; a spinner replaces it while options load |
| menu | the matching options, as wide as the field; opens below, or above near the bottom of the screen |
| option | one row; a check marks the chosen one |
| group header | an optional heading over a run of options; hidden when none of its options match |
| message row | "No results found", a loading hint, or a load error; it can't be chosen |
The menu shows about five and a half rows, then scrolls.
Sizes
<tk-combobox aria-label="City" placeholder="Start typing a city"> <option>Amsterdam</option> </tk-combobox> <tk-combobox size="large" aria-label="Airport" placeholder="Start typing an airport"> <option>Schiphol</option> </tk-combobox>
size |
Height | Padding-inline | Text | Option row |
|---|---|---|---|---|
medium (default) |
32px | space-8 |
Body Small (14/20) | 32px |
large |
36px | space-10 |
Body Medium (16/24) | 36px |
The heights match Select, Input, and Button. Long options are cut off with an ellipsis.
States
<tk-combobox aria-label="Office" value="lis" disabled> <option value="lis">Lisbon</option> </tk-combobox> <tk-combobox aria-label="Team" value="core" readonly> <option value="core">Core platform</option> </tk-combobox>
Hover doesn't change the field. Keyboard focus colors its border and thickens it; there is no ring.
disabled: can't be focused or opened, and isn't submitted.readonly: shows the value and submits it. People can focus and copy it, but can't edit it or open the menu. The clear button and chevron are hidden.- Error: set
erroron the Field. The message appears below and the border turns red, even while focused.
Filtering
- Typing opens the menu and filters it. Matching ignores case and finds text anywhere in the label: "kingdom" finds "United Kingdom". Descriptions and group headers aren't searched.
- The text is a search, not the value. The value changes only when someone chooses an option. If they leave without choosing, the text goes back to the chosen option.
- A click opens the full list, so people can choose without typing.
- With no match, the menu shows "No results found".
Data sources
| Source | How | Filtering |
|---|---|---|
<option> children (default) |
markup, with <optgroup> for groups |
in the browser |
options property |
an array of { value, label, description?, disabled?, group? } |
in the browser |
loadOptions property |
an async function that returns options for a query | on your server |
Use loadOptions only when the list is remote or too large to ship:
combobox.loadOptions = async (query, { signal }) => {
const res = await fetch(`/api/cities?q=${encodeURIComponent(query)}`, { signal });
return (await res.json()).map((c) => ({ value: c.id, label: c.name }));
};- Requests wait
debouncems (250) after the last keystroke. min-charsholds requests until enough is typed; the menu says "Type 2 or more characters." The default, 0, loads the full list on click.- While loading, a spinner replaces the chevron. If the promise rejects, the menu shows "Couldn't load options." with Try again; Enter also retries.
- Only the latest query's results show. Pass
signaltofetchto cancel older requests. - Return the matches already filtered, one line each.
Option descriptions
<tk-combobox aria-label="Plan" placeholder="Start typing a plan"> <option value="free" data-description="For trying things out">Free</option> <option value="pro" data-description="Adds version history">Pro</option> </tk-combobox>
data-description (or description in options) adds a short second line. Use it to
help people choose; don't describe every option. Screen readers read it after the label.
Creatable
<tk-field label="Tag">
<tk-combobox creatable placeholder="Start typing a tag">
<option value="design">Design</option>
<option value="research">Research</option>
</tk-combobox>
</tk-field>By default people can only choose your options. With creatable, a query that matches no
option adds a Create "…" row. Choosing it, or pressing Enter on the typed text, sets that
text as the value and adds it to the list. Use it for open sets, such as tags or skills.
- An exact match, ignoring case, offers the option instead of creating a copy.
- Leaving the field with Tab, Esc, or a click outside creates nothing.
- It works with every data source, but not while loading or below
min-chars.
When to use
- Use a combobox for a long list, from about 15 options up, or a list with no fixed end, such as cities or products.
- Use a
Select for about 4 options up to a screenful. A select is simpler; use a combobox only when it no longer fits. - Use a
Country select for countries. The list, names, and flags are built in. - Use a
Multiselect when people can pick more than one. - Use an
Input when any text is a valid answer. - Don't use a combobox for search. It sets a form value; it doesn't show results.
Content
- Label: name what you're asking for, in sentence case, with no colon.
- Placeholder: "Start typing a…". It disappears once people type, so don't put anything they need in it.
- Options: one short line each. Put the word people will type first: "Congo (Democratic Republic)", not "Democratic Republic of the Congo".
- Don't start options with an icon or emoji. They get in the way of filtering.
- Error: say how to fix it, using the label's words: "Select a country from the list".
More in
Do / Don't
| ✅ Do | ❌ Don't |
|---|---|
| Use a combobox for long lists | Use it where a select or radio group fits |
| Let people open the full list with a click | Require typing before anything shows |
| Put the distinguishing word first | Start options with icons or emoji |
| Use it to set a form value | Use it as a search box |
| Label it with a Field | Use the placeholder as the label |
Related
Field : the label, helper, and error around a combobox.Select : one option from a shorter list.Country select : a combobox with the country list built in.Multiselect : several options, with the same filtering.Input : free text.Select family : how these compare.
API
Attributes
| Attribute | Type | Default | What it does |
|---|---|---|---|
value |
string | '' |
the chosen option's value; '' means none. Typing doesn't change it |
placeholder |
string | — | the hint shown while the field is empty |
name |
string | — | the form field name |
required |
boolean | false |
an empty value blocks form submission; no visible marker (the Field adds the asterisk) |
disabled |
boolean | false |
not focusable, can't open, left out of the form |
readonly |
boolean | false |
focusable and submitted, but can't be edited or opened |
size |
medium | large |
medium |
height 32 / 36px |
creatable |
boolean | false |
lets people add a value that isn't in the list |
min-chars |
number | 0 |
with loadOptions: characters needed before loading |
debounce |
number | 250 |
with loadOptions: milliseconds to wait after typing |
There is no autocomplete attribute: browser autofill stays off so its menu doesn't cover
the combobox menu.
Properties
| Property | Type | What it does |
|---|---|---|
options |
{ value, label, description?, disabled?, group? }[] |
options from JavaScript; replaces the <option> children |
loadOptions |
(query, { signal }) => Promise<options> |
loads options per query; takes over from options and children |
Slots
| Slot | Content |
|---|---|
| default | <option> and <optgroup> elements; changes to them are picked up |
<option> takes value, selected, disabled, and data-description. <optgroup>
takes label.
Events
change fires when someone chooses an option or clears the field. It doesn't fire while
they type or move through the menu, or when you set value in code.
In a form, the value submits under name, a form reset restores the starting value, and
Enter on the closed field submits the form.
CSS
::part(field): the field.::part(listbox): the menu.
Tokens
The field, menu, options, error, and focus use
| Part | Tokens |
|---|---|
| typed or chosen text | text/default; placeholder text/placeholder |
| clear button | icon/default, icon-size-16; hover state/hover, pressed state/press; 24×24px, radius-full |
| message row | text/secondary; load error text/error, Try again text/accent |
Create "…" row |
text/secondary; the typed text text/default |
Accessibility
Built in:
- Screen readers hear an editable combobox with suggestions, and how to use them.
- A hidden status message announces how many options match as people type ("7 results available.", "No results."), and the loading state and errors.
- Focus stays in the field while the menu is open. Each highlighted option is read with its position, such as "2 of 5".
- Nothing is chosen until people choose it. Esc and a click outside change nothing.
- The clear button isn't a Tab stop. To clear from the keyboard, delete the text and press Enter or Tab.
Your part:
- Give every combobox a name: a Field label or
aria-label. - Keep the full list reachable by click; with
loadOptions, keepmin-charsat 0 when you can. - Write errors as the Content rules say.
| Key | Menu closed | Menu open |
|---|---|---|
letters, Space |
type; the menu opens and filters | type; the list filters |
Down / Up |
opens the full list and highlights the chosen or first option | moves the highlight, skipping disabled options; stops at the ends |
Alt + Down |
opens without a highlight | — |
Alt + Up |
— | chooses the highlighted option and closes |
Enter |
submits the form | chooses the highlighted option; with none highlighted, does nothing |
Esc |
— | closes and restores the text; a surrounding dialog stays open |
Tab |
moves to the next control | chooses the highlighted option, or restores the text, and moves on |
Home / End, Left / Right |
move the cursor in the text | move the cursor in the text |
Contrast limits
- Field border: 1.33:1 in light and 2.00:1 in dark, under the 3:1 minimum; 4.22:1 in high contrast. Always show a label.
- Placeholder: 3.26:1 in light, under the 4.5:1 text minimum. Don't put anything people need in it.