---
## Combobox
Searchable menu (autocomplete) combining a text input with a
filterable option list. Implements the ARIA combobox pattern with keyboard
navigation, custom filtering, and two-way bindable state. Single-select by
default; pass `multiple` for an array-bound multi-select that renders picks as
removable tag chips.
**Import:** `import { Combobox } from '@urbicon-ui/blocks';`
### Examples
```svelte
```
```svelte
({ label: t.name, value: t.id }))}
bind:value={tenantId}
/>
```
```svelte
```
```svelte
```
```svelte
```
### Variants
- variant: bare, filled, ghost, outlined, underline (default: outlined)
- size: lg, md, sm, xl, xs (default: md)
- disabled: true (default: false)
- error: true (default: false)
- open: true (default: false)
- required: true (default: false)
- tier: commit, modify (default: modify)
### Api
| Prop | Type | Required | Default | Description |
| --- | --- | :---: | --- | --- |
| allowCustom | `T extends string ? boolean : never` | no | false | Open the closed option list: whatever the user types becomes selectable as a value of its own, with `options` / `groups` / `queryFn` left as the suggestion set. A trailing row ("Use “Kino 46”", localized) appears once the trimmed query is non-empty and no option the field knows carries that label case-insensitively — including `groups`, `queryFn` results and the labels behind the current selection, but not a `disabled` option, whose label names nothing that can be picked. It is an ordinary option: the arrow keys reach it, Enter picks it, and `onValueChange` receives the trimmed query. In multi-select it obeys `maxItems` like any unselected option — at the cap it renders disabled and Enter on it does nothing until a tag is removed. While an async `queryFn` request is in flight no row is offered; it returns with the results. **String values only.** The selected value is the query text, so the prop is typed away for a numeric or boolean `T` — `Combobox` cannot mint a number out of free text, and a caller who needs one parses the string themselves in `onValueChange`. The stored label is the raw query, not the row's prompt: the input (single mode) and the tag (multi) read "Kino 46". A value picked this way therefore needs no `seedOptions` entry on a later mount, and the DEV orphan warning stays quiet for every string value while this is set. In `queryFn` mode a pre-bound single value is exempt: there it is an id whose label is still coming from the server, so it follows the async rule and needs `seedOptions` until the results supply the label. `customOption` is not called for this row — it renders the built-in prompt, since the option behind it is in none of your arrays. For a row that renders differently, or a value that is not the query text, leave `allowCustom` off and append an option of your own instead. |
| class | `string` | no | | Extra classes merged onto the root wrapper element. In the same Tailwind bucket `class` wins over the `slotClasses` entry for the element it lands on. Presets and provider defaults sit below both. |
| clearable | `boolean` | no | false | Show a clear button in place of the chevron while a value is selected. Clicking it resets the value (`null`, or `[]` in multi mode); Escape only closes the list. |
| closeOnClickOutside | `boolean` | no | | Whether the listbox closes on outside click. Default `true`. Set to `false` to pin the listbox open while the consumer manages dismissal explicitly. |
| closeOnEscape | `boolean` | no | | Whether the listbox closes on Escape key. Default `true`. Set to `false` for inline contexts where Escape should be intercepted by an outer widget (e.g. a row editor that wants to revert on Escape). |
| customOption | `Snippet<[ComboboxOption, boolean]>` | no | | Custom option renderer. Receives the option and whether it is selected. |
| customTag | `Snippet<[ComboboxOption, () => void]>` | no | | Custom tag renderer replacing the default chip. Receives the selected option and a `remove` callback — call it to drop the tag (fires `onRemoveTag` + `onValueChange`). Use it to render a `` or any bespoke chip. Positional args: `(option, remove)`. |
| debounceMs | `number` | no | 250 | Debounce applied to `queryFn` in milliseconds. |
| disabled | `boolean` | no | false | Disable the entire combobox. |
| error | `string` | no | | Error message — replaces helper text, flags the field as invalid (`aria-invalid`) and paints the shared danger frame on the input (single mode) or the tokenizer control (multi). The string prop shadows the boolean `error` variant axis, which the component derives from it (mirrors Input / Select) — that is what the `Omit` above is for. |
| filter | `(option: ComboboxOption, query: string) => boolean` | no | | Custom filter replacing the built-in case-insensitive label match. Return `true` to include an option. Ignored when `queryFn` is set (the server filters). |
| groups | `ComboboxGroup[]` | no | | Grouped options with section labels, at parity with Select. Takes precedence over `options` when set. Filtering runs per group; groups whose options all filter out are hidden, and keyboard navigation flows across the flattened, still-visible options exactly as it does for a flat list. |
| helper | `string` | no | | Helper text shown below the field. Hidden when an error is set. |
| id | `string` | no | | Deterministic HTML `id` for the component. Auto-generated when omitted. |
| label | `string` | no | | Field label rendered above the input. |
| loadingText | `string` | no | 'Loading…' | Text shown in the listbox while an async `queryFn` request is in flight. |
| maxItems | `number` | no | | Cap the number of selected values. Once reached, options that aren't already selected become non-selectable (and are skipped by keyboard navigation) until a tag is removed; already-selected options can still be toggled off. |
| mint | `MintProp` | no | 'none' | Micro-interaction preset applied to the search input (the `role="combobox"` element). Only applies while not disabled. |
| multiple | `'false' | 'true'` | no | | Single-select mode (the default). Omit or set to `false` explicitly. |
| name | `string` | no | | Shared `name` for a hidden input for native form submission. In multi-select mode one hidden input is emitted per selected value. |
| noResultsText | `string` | no | 'No results found' | Text displayed when the filter produces no matches. |
| onClickOutside | `() => void` | no | | Fires after an outside click closes the listbox. Use for analytics or side-effects on dismiss. Does NOT control whether the listbox closes — that is governed by `closeOnClickOutside`. |
| onError | `(error: unknown) => void` | no | | Fired when `queryFn` rejects (aborted / superseded requests are ignored). The loading state ends and the previous option list stays in place — use this to surface the failure (toast, inline message). Without a handler the rejection is logged DEV-only (`console.warn`) and swallowed in production; it never escapes as an unhandled promise rejection. |
| onEscape | `() => void` | no | | Fires after Escape closes the listbox. Use for analytics or to clear ephemeral state on dismiss. Does NOT control whether the listbox closes — that is governed by `closeOnEscape`. |
| onOpenChange | `(open: boolean) => void` | no | | Fires when the listbox opens or closes from user interaction (focus, typing, chevron toggle, selection, Escape, outside click). Receives the new open state — use it e.g. to lazy-load options on first open. Not called when the consumer writes `bind:open` directly. |
| onRemoveTag | `(value: T) => void` | no | | Fires when a single value is removed from the selection — via the tag's remove button, Backspace on an empty query, or toggling a selected option off. Receives the removed value. `onValueChange` fires alongside it with the resulting array; use `onRemoveTag` for per-tag side effects (analytics, exit animations). Note: the bulk clear button does NOT fire this per tag — it signals through `onValueChange([])` only. |
| onValueChange | `(value: T | null) => void` | no | | Fires after the selected value changes. Receives the new value or `null` on clear. |
| open | `boolean` | no | false | Controls the open state of the listbox. Supports `bind:open`. |
| options | `ComboboxOption[]` | no | | Array of selectable options. Each needs a unique `value`. |
| placeholder | `string` | no | 'Search…' | Placeholder shown when the input is empty. |
| preset | `string` | no | | Apply a named preset registered via ``. Prefer this over `class` overrides when the requested look falls outside the semantic intent palette — presets keep hover/active/dark-mode logic coherent and make the custom look reusable across the project. |
| query | `string` | no | | Current search query text. Supports `bind:query` for external control (e.g. server-side filtering). |
| queryFn | `(query: string, signal: AbortSignal) => Promise[]>` | no | | Server-side search (analogous to the Table remote-mode API). When set, the Combobox stops filtering client-side and instead calls `queryFn` — debounced by `debounceMs` — on each query change, replacing the option list with the resolved result. The `AbortSignal` is aborted when a newer query supersedes an in-flight request, so a slow stale response never clobbers a fresh one. Aborted rejections are swallowed; other rejections end the loading state, leave the previous options in place, and are reported via `onError`. `options`/`groups` are ignored in this mode. The selected option's label is cached so it survives result sets that no longer contain it. |
| required | `boolean` | no | false
In multi-select mode this is visual only — the transient search input
carries no native `required` (it is cleared after each pick), so enforce a
minimum selection in your submit handler. | Marks the field as required. Adds the asterisk on the label. |
| seedOptions | `ComboboxOption[]` | no | | Label seed for pre-selected values whose options are not (yet) in the option list — the async-mode pattern of binding `value` on mount before any `queryFn` result has arrived. Consulted as the LAST lookup source when resolving a selected value's label (current options first, then the pick-cache, then this seed), so it can never shadow a live option, and it works identically for single and multi selection. Without a matching seed such a value renders as its raw `String(value)` (and warns DEV-only). Declarative and idempotent — not a second selection source: `value` alone decides what is selected; `seedOptions` only supplies labels. A string value under `allowCustom` needs no seed and raises no DEV warning — there the value is its own label, in the input and on the tag alike. The exception is `queryFn` mode, where a pre-bound single value still needs a seed until the server supplies its label. |
| slotClasses | `Partial>` | no | | Per-slot class overrides merged with tv() styles. Slots: base | label | requiredMark | inputWrapper | input | message | helper | listbox | option | optionActive | optionSelected | optionHint | optionCheck | group | groupLabel | loading | noResults | clear | chevron | control | search | tag | tagLabel | tagRemove. In the same Tailwind bucket `class` wins over the `slotClasses` entry for the element it lands on. Presets and provider defaults sit below both. |
| unstyled | `boolean` | no | false | Remove all default tv() classes — only user-provided classes apply. |
| value | `T[]` | no | [] | Currently selected values. Supports `bind:value`. |
| variant | `ComboboxVariants['variant']` | no | 'outlined' | Visual style, at parity with Input / Textarea / Select. - `outlined` (default) — visible border, surface-base background - `filled` — surface-interactive fill, no border - `ghost` — transparent until hover/focus - `underline` — bottom-line only, no border-box - `bare` — no frame, no fill, no padding, no fixed height: an inline autocomplete that reads as the text it sits in. `size` keeps only the type step, and focus is an outline in `--blocks-focus-ring-color` |
| size | `'lg' | 'md' | 'sm' | 'xl' | 'xs'` | no | md | Controls the dimensions, padding, and text size of the Combobox. Affects the component's physical footprint. Available options: lg, md, sm, and 2 more. |
| tier | `'commit' | 'modify'` | no | modify | Selects the semantic radius tier of the Combobox — the shape family it belongs to (`--radius-commit`/`-modify`/`-contain`/`-bridge`). Shape is retuned per family in your theme, so this picks the family rather than a pixel value. Available options: commit, modify. |
### Types
```ts
interface ComboboxOption {
label: string;
value: T;
disabled?: boolean;
/**
* Trailing secondary text on the option row — a facet count ("24"), a unit,
* a price. It is part of the option's text content, so the row's accessible
* name reads "Noir 24" rather than dropping the number.
*
* A string, not a number: formatting and locale belong to the caller
* (`String(count)`, or an `Intl.NumberFormat` result). Ignored while
* `customOption` draws the row — that snippet owns the whole row. Not matched
* by the built-in filter, which reads `label` only.
*/
hint?: string;
}
```
```ts
interface ComboboxGroup {
label: string;
options: ComboboxOption[];
}
```
```ts
type ComboboxProps = | ComboboxSingleProps
| ComboboxMultipleProps
```
```ts
type ComboboxOptionType = ComboboxOption
```
```ts
type MintProp = | MintName
| { name: MintName; config?: MintConfig & Record }
| Array
| Array }>
```
```ts
type SelectValue = string | number | boolean
```
```ts
type MintName = BuiltinMintName | 'none' | (string & {})
```
```ts
interface MintConfig {
/**
* `hover` and `focus` hold the effect while the pointer/visible focus stays
* on the element; `click` and `load` run it once.
*/
trigger?: 'hover' | 'click' | 'focus' | 'load';
/**
* Effect duration in ms. Written as an inline per-effect custom property
* (`--blocks-mint--duration`), so the CSS transition/animation
* actually runs at this speed; unset, the theme duration tokens apply.
*/
duration?: number;
/** Delay in ms before the effect applies. */
delay?: number;
/**
* CSS easing for the effect (`--blocks-mint--easing` inline);
* unset, the per-effect theme default applies.
*/
easing?: string;
disabled?: boolean;
}
```
```ts
type BuiltinMintName = (typeof BUILTIN_MINT_NAMES)[number]
```
### Slots (slotClasses keys)
`base`, `label`, `requiredMark`, `inputWrapper`, `input`, `message`, `helper`, `listbox`, `option`, `optionHint`, `optionActive`, `optionSelected`, `optionCheck`, `group`, `groupLabel`, `noResults`, `loading`, `clear`, `chevronButton`, `chevron`, `control`, `search`, `tag`, `tagLabel`, `tagRemove`