form fields

<tc-combobox>

Searchable, optionally multi-select dropdown with tag chips and per-option icons. The "select N from many" widget that the native select multiple isn't.

import "@ra9/tan-compose-kit/combobox"

Props

NameTypeDefaultDescription
valuestring""Comma-separated values. A single value is just the value itself; multiple values are joined with commas. Also accepts a JSON array when set programmatically.
namestring""Form field name. Used for FormData entries on submit.
optionsArray&lt;OptionDef&gt;[]Each entry is { value, label, icon?, disabled?, group? }. icon may be any short string — emoji flag, single glyph, etc.
multiplebooleanfalseEnables chip-based multi-select. Selecting an option toggles it.
searchablebooleantrueWhen true (default) the user can type to filter options.
placeholderstring""Empty-control hint.
empty-textstring"No results"Text shown in the popup when no options match the filter.
labelstring""Form field label.
helperstring""Helper text shown below the control.
errorstring""Error text. When set, the control turns red and the helper slot shows this text instead.
disabledbooleanfalseDisables the control. Reflects to the host attribute.
requiredbooleanfalseMarks the field as required. Reflects to the host attribute.
maxnumber0Maximum number of selections in multiple mode. 0 means unlimited.

Events

EventDetailWhen
tc-change{ value: string | string[] }Fires when the selection changes. value is the array of selected values in multiple mode, the string otherwise.
tc-search{ query: string }Fires on every keystroke in the search field. Useful for async option fetch.
tc-openFires when the popup opens.
tc-closeFires when the popup closes.

CSS variables

VariableDefaultDescription
--tc-input-bgvar(--tc-color-surface, #ffffff)Background of the control.
--tc-input-bordervar(--tc-color-rule-strong, #d9cfb8)Default border color.
--tc-input-border-focusvar(--tc-color-accent, #a16939)Border color when the popup is open or the control is focused.
--tc-input-errorvar(--tc-color-danger, #b3261e)Border + helper color when error is set.
--tc-combobox-chip-bgvar(--tc-color-accent-soft, #efe2cf)Background of selected-value chips in multiple mode.
--tc-combobox-chip-fgvar(--tc-color-accent-hover, #8a572d)Text color of chips.
--tc-combobox-popup-bgvar(--tc-color-surface, #ffffff)Popup background.
--tc-combobox-popup-hovervar(--tc-color-accent-soft, #efe2cf)Background of the focused / hovered option.
--tc-combobox-popup-activevar(--tc-color-accent-soft, #efe2cf)Background of selected options in the popup.

Examples

Single-select with icons

The simplest case. options is an array of { value, label, icon? }. Icons can be any short string — emoji flags, glyphs, anything that fits in a 16-px box.

const cb = document.querySelector("tc-combobox"); cb.options = [ { value: "US", label: "United States", icon: "🇺🇸" }, { value: "GB", label: "United Kingdom", icon: "🇬🇧" }, { value: "JP", label: "Japan", icon: "🇯🇵" }, ]; cb.addEventListener("tc-change", (e) => { console.log(e.detail.value); // "US" });

Multi-select with tag chips

Pass multiple and selections accumulate as chips. The × on each chip removes it; Backspace in the empty search field also removes the last chip. Selecting an already-selected option toggles it off.

<tc-combobox label="Countries" name="countries" multiple></tc-combobox>

In multiple mode, the form submits one entry per selected value (when name is set). Reading el.value returns a comma-separated string; e.detail.value in tc-change is the full array.

No icons — plain tag picker

icon is optional. Drop it entirely for tag pickers, role pickers, or any flat list.

tagCombo.options = [ { value: "design", label: "Design" }, { value: "frontend", label: "Frontend" }, { value: "backend", label: "Backend" }, ];

The max prop limits the selection count in multiple mode. Once reached, additional options ignore clicks.

Async option fetch

Combobox doesn't ship a built-in async-fetch behavior, but the tc-search event makes the typical pattern small. Debounce the query, fetch, then assign the result.

let timer; const cb = document.querySelector("tc-combobox"); cb.addEventListener("tc-search", (e) => { clearTimeout(timer); timer = setTimeout(async () => { const res = await fetch(`/api/users?q=${encodeURIComponent(e.detail.query)}`); cb.options = await res.json(); }, 200); });

Form submission

Combobox is form-associated. Multi-select sets the form value via a FormData carrying one entry per chosen option; single-select sets a single string.

<form> <tc-combobox name="countries" multiple required></tc-combobox> <tc-button type="submit">Submit</tc-button> </form> form.addEventListener("submit", (e) => { const data = new FormData(e.target); console.log(data.getAll("countries")); // ["US", "FR", "JP"] });

Theming

Combobox reuses the --tc-input-* tokens for its outer chrome, so it sits next to <tc-input> and <tc-select> cleanly. The chip and popup colors live in their own --tc-combobox-* tokens so you can tone them without disturbing the rest of the form.

tc-combobox { --tc-combobox-chip-bg: #e6f0ff; --tc-combobox-chip-fg: #1c3a7a; }

Accessibility

  • The search input gets role="combobox" and aria-expanded reflecting the popup state.
  • The popup is role="listbox" and (in multi mode) aria-multiselectable="true".
  • Each option gets role="option" and aria-selected.
  • Keyboard: ↑/↓ navigates, Enter selects, Esc closes, Backspace on empty search removes the last chip.
  • Disabled options carry aria-disabled="true" and don't receive focus or activate.