Skip to main content
Urbicon UI

Combobox

Searchable autocomplete input for choosing from a long list of options.

Playground

Tier Style variant
<script lang="ts">
  import { Combobox } from '@urbicon-ui/blocks';

  const options = [
    { label: 'United States', value: 'us' },
    { label: 'United Kingdom', value: 'uk' },
    { label: 'Germany', value: 'de' },
    { label: 'France', value: 'fr' },
    { label: 'Japan', value: 'jp' },
    { label: 'Australia', value: 'au' },
    { label: 'Canada', value: 'ca' },
    { label: 'Brazil', value: 'br' }
  ];
</script>

<Combobox
  {options}
  placeholder="Search countries…"
/>

01 Examples

Each option is an object with a label and a value, and options is an array of them. Bind a single selection with bind:value (the picked value, or null when empty), or pass multiple to bind an array rendered as removable tags. Reach for Combobox over Select when the list is long enough to search or its values load from a server. Select suits a short, fixed set.

Multi-select with tags

Pass multiple to bind an array of values. Picks render as removable tag chips and the listbox stays open across selections. maxItems caps the count, and Backspace on an empty field removes the last tag.
TypeScript Svelte
<Combobox
  label="Skills"
  options={languages}
  multiple
  bind:value={skillsValue}
  maxItems={5}
  placeholder="Add skills…"
  clearable
/>

Helper, error & required

Combobox follows the same form-field contract as Input and Select. error overrides helper when both are set.
We use this to schedule meetings
<Combobox
  label="Timezone"
  options={timezones}
  bind:value={timezoneValue}
  placeholder="Search…"
  helper="We use this to schedule meetings"
  required
  clearable
/>
<Combobox
  label="Primary language"
  options={languages}
  error="Please select your primary language"
  placeholder="Search…"
/>

Custom filter

Replace the default case-insensitive contains-match with your own predicate. Here, strict startsWith matching suits command-style input.
<Combobox
  label="Language"
  options={languages}
  bind:value={filterValue}
  placeholder="Type to match…"
  filter={(opt: ComboboxOption, q: string) =>
    opt.label.toLowerCase().startsWith(q.toLowerCase())}
/>

Custom option renderer

Use the customOption snippet for rich list items: an avatar, the name, and a role badge on each row.
<Combobox
  label="Team member"
  options={teamMembers}
  bind:value={customValue}
  placeholder="Search team…"
  clearable
>
  {#snippet customOption(opt: ComboboxOption, isSelected: boolean)}
    <div class="flex w-full items-center gap-3">
      <Avatar src={avatars[opt.value]} size="xs" />
      <span class="flex-1 truncate text-sm">{opt.label.split('')[0]}</span>
      <Badge
        size="xs"
        variant="soft"
        intent={isSelected ? 'success' : 'neutral'}
        class="shrink-0"
      >
        {opt.label.split('')[1]}
      </Badge>
      {#if isSelected}
        <CheckIcon size={14} class="text-primary shrink-0" />
      {/if}
    </div>
  {/snippet}
</Combobox>

02 Free Text

By default the option list is closed: a value that is not in it cannot be picked. Pass allowCustom and the list becomes a set of suggestions instead — once the query matches no option's label, a trailing row offers to keep what was typed. It behaves like any other option: the arrow keys reach it, Enter picks it, and onValueChange receives the typed text. What is stored is the text itself, not the row's wording, so the field reads “Kino 46” afterwards and the row's label is translated with the rest of the library.

Suggestions, not a closed list

A venue field: the houses someone goes to often are suggestions, every other name is typed. clearable resets it, and the value is a plain string either way — pick one from the list or invent one, the binding does not change.

Value: null

<Combobox
  label="Venue"
  options={venues}
  bind:value={venueValue}
  allowCustom
  clearable
  placeholder="Search or type a venue…"
/>
<p class="text-text-tertiary text-xs">
  Value: <code class="text-text-primary">{venueValue ?? 'null'}</code>
</p>

The suggestions may come from anywhere — options, groups or a queryFn; the row waits for the results and then sits below the last group. It always draws itself, so customOption never receives it — the option behind it is in none of your arrays. If the row should look different, or store something other than the typed text, leave allowCustom off and append an option of your own instead.

04 Customization

Frosted glass

slotClasses tints the input and listbox into a glass look. It keeps the field's radius tier, spacing and keyboard behaviour. Only the fill, border and blur change, in raw colours because glass has no token equivalent.
<Combobox
  aria-label="Timezone"
  options={timezones}
  placeholder="Select your timezone…"
  slotClasses={{
    input:
      'border-white/20 bg-white/10 text-white placeholder:text-white/60 backdrop-blur-md hover:border-white/30 focus-visible:border-white/40 focus-visible:bg-white/15',
    listbox: 'border-white/20 bg-white/10 text-white backdrop-blur-xl',
    option: 'text-white/80',
    optionActive: 'bg-white/20 text-white',
    optionSelected: 'bg-white/15 text-white',
    optionCheck: 'text-white',
    noResults: 'text-white/60',
    chevronButton: 'text-white/60 hover:text-white'
  }}
/>

A field that should read as the text it sits in is the bare variant, not a stack of reset classes: no frame, no fill, no padding, no fixed height, and size keeps only its type step. It needs context that says it is a field — a placeholder, a rule under the line, a label before it — and it keeps the one thing such a reset usually loses, a focus outline, whose colour is the --blocks-focus-ring-color custom property. Worked through on the Input page.

This is one of five ways to restyle a block. See Customization for class, slotClasses, unstyled, preset and provider-level overrides.

05 Accessibility

Built-in ARIA

The input uses role="combobox" with aria-expanded, aria-controls, and aria-autocomplete="list". The listbox uses role="listbox" and each option uses role="option" with aria-selected.

Keyboard

/ to navigate options. Enter to select. Escape to close. Home / End to jump to first / last option. Disabled options are skipped during navigation.

Active Descendant

Focus stays on the input at all times. The visually highlighted option is communicated via aria-activedescendant, keeping screen readers synchronized without moving DOM focus.

06 API Reference

41 props
41 props
Prop
Type
Default
Description

07 Types

Local type definitions used by this component.

13 types
Name
Kind
Category
Used by
Description

08 Installation

Import

import { Combobox } from '@urbicon-ui/blocks';