Combobox
Finds one or more values in a long list by typing.
Examples
maya@acme.example
ana@acme.example
Paste a list from anywhere.
late-delivery
Mode
singledefault
- search_articleslookup_order
2 tools allowed.
multiple
- late-delivery
creatable
- ana@acme.example
email
Sizes
sm
defaultdefault
lg
States
rest
hover
filled
invalid
disabled
pending
open
empty
Usage
When to use it
- Use
- More than about seven options, so typing beats scanning: agents, models, queues, tools.
- Use
- Several values from a list, shown together: tools an agent may call, labels on a conversation.
- Use
- Free values the list cannot know: new labels, the emails allowed to review a draft.
- Not for
- Seven or fewer options and no typing needed. Use Select.
- Not for
- Two to five options that should all stay visible. Use Segmented control.
- Not for
- A search across the product that runs commands. Use Command.
- Not for
- One line of free text with no list. Use Input.
Rules
8 rules
- Must
- Set multiple creatable with string items, and a validate or preset when the format matters. Because creatable stores typed entries as strings, and without validate a typo like "ana@" commits as a valid chip.
- Must
- Pass pending while items load and filter={null} so the server order stands. Because without pending the list says nothing matched while results load, and the client filter reorders and drops server matches.
- Should
- Set showClear on an optional single field; it has no empty option. Because once a value is picked there is no other way back to empty.
- Should
- Name what was searched in the empty text, as 'No agents match.'
- Must
- Pass the value name as a plain string child of ComboboxChip. Because the remove button is named from string children only, so a chip holding an element gets a button called just "Remove".
- Never
- Use a combobox for 7 options or fewer; use Select. Because typing to filter a list that already fits on screen is an extra step, and Select shows every option at once.
- Must
- Label the field with FieldLabel or aria-label. Because a placeholder is not a name and disappears on the first keystroke, so a screen reader hears "combobox".
- Never
- Lift the chips field height cap; it stops at four rows (176px) and scrolls. Because a pasted list of 200 emails would otherwise grow the field and push the rest of the form off the screen.
Accessibility
7 checks
- The input is role="combobox" with aria-expanded, aria-controls, aria-activedescendant and aria-invalid.
- The list is a listbox (multiselectable with multiple); items are options with aria-selected.
- ArrowDown and ArrowUp open and move; Enter picks or adds; Escape closes, a second Escape reaches the dialog.
- Backspace on an empty chips input removes the last chip; ArrowLeft at the start moves into the chips.
- Adds, removes and Searching are announced in polite status regions.
- A marked chip says its reason in title and sr-only text, so color is not the only cue.
- The chip remove button reads "Remove" plus the value; the chevron trigger reads "Show options" and stays out of the tab order.
Edge cases
13 edge cases
- Long chip
- Truncates inside the field; a 36-character email fits 320px.
- One-letter chip
- Keeps a 24px remove button so neighbours stay separate targets.
- items=[]
- Not creatable: the empty text. Creatable: nothing until the person types.
- 24 chips
- Wrap; the field caps at 176px and scrolls; the popup caps at the available height.
- Right to left
- Chips flow right to left; the check sits at the inline end.
- Slow search
- Keeps the Searching row and the old items until the query changes.
- Invalid email
- Kept as a marked chip; the field turns invalid and announces the count.
- Pasted list
- Splits into chips; a quoted `"Dee, Ops" <dee@acme.example>` stays one address.
- Enter held down
- Adds once; a repeat clears the text and flashes the existing chip.
- Inside a dialog
- Escape closes the list first, then the dialog.
- Inside a form
- Enter picks or adds and does not submit while the list is open.
- Forced colors
- The field and chips draw 1px system borders; the highlight gets an outline.
- Reduced motion
- The popup opens without a transition; the spinner stops.
API
9 props
Combobox items, value, defaultValue, onValueChange, multipleBase UIcreatablebooleandefaultfalsepreset'email'normalize, validate, split(raw) => string, (v, all) => string | null, (text) => string[]defaulttrim, none, commas and linesdelimitersstring[]default[',']pendingbooleandefaultfalseComboboxFielditems, multiple, placeholder, emptyText, size, showClearComboboxInput size, showSearchIcon, showTrigger, showClear'sm' | 'default' | 'lg', booleandefault'default'ComboboxChip value, showRemovestring, booleandefault, true
Tokens
26 tokens
--line-control- oklch(var(--ink-c) / 0.16)
--line-frame- oklch(var(--ink-c) / var(--line-frame-alpha))
--accent-wash- var(--surface-selected)
--accent-mark- var(--ink)
--line-focus- 2px
--focus-halo- 0 0 0 3px oklch(var(--ink-c) / 0.08)
--border-focus- var(--ink)
--surface-pressed- oklch(var(--ink-c) / 0.08)
--radius-inner- 4px
--radius-soft- 6px
--type-label-sm- 400 12px/16px var(--font-sans)
--surface-inverse- var(--ink)
--text-inverse- var(--paper)
--fail-text- oklch(51% 0.180 26)
--surface-page- var(--paper)
--elev-overlay- 0 0 0 1px var(--line-frame), 0 12px 24px -12px oklch(var(--text-on-field-c) / var(--shadow-alpha))
--dur-base- 160ms
--dur-fast- 120ms
--ease-step- cubic-bezier(0.2, 0, 0, 1)
--surface-hover- oklch(var(--ink-c) / 0.05)
--type-label- 400 14px/16px var(--font-sans)
--type-tag- var(--type-code-sm)
--text-2- oklch(var(--ink-c) / 0.80)
--field-max-height- calc(var(--control-lg) * 4)
--dur-spin- 800ms
--ease-spin- linear