Skip to content
omaris

Inputs

Combobox

Combobox — type to search, arrow to the one you want, Enter to take it.

import { Combobox } from 'omaris'
Learn

Examples

Basic

Type to filter, arrow to the one you want, Enter to take it — the field for a list too long to scroll, like a country or a person.

<script lang="ts">	import { Combobox } from 'omaris';​	let country = $state('iq');​	const COUNTRIES = [		{ value: 'iq', label: 'Iraq', keywords: ['baghdad', 'erbil'] },		{ value: 'tr', label: 'Türkiye', keywords: ['turkey', 'ankara'] },		{ value: 'jo', label: 'Jordan', keywords: ['amman'] },		{ value: 'ae', label: 'United Arab Emirates', keywords: ['dubai', 'uae'] },		{ value: 'sa', label: 'Saudi Arabia', keywords: ['riyadh'] }	];</script>​<Combobox class="w-72 max-w-full" label="Country" options={COUNTRIES} bind:value={country} />

Keywords

Matching is accent-insensitive and looks at keywords as well as the label, so "turkey" finds Türkiye and "uae" finds the Emirates.

<script lang="ts">	import { Combobox } from 'omaris';​	let value = $state('');​	const AIRPORTS = [		{ value: 'bgw', label: 'Baghdad International', description: 'BGW', keywords: ['iraq'] },		{			value: 'ebl',			label: 'Erbil International',			description: 'EBL',			keywords: ['iraq', 'hawler']		},		{			value: 'ist',			label: 'İstanbul Airport',			description: 'IST',			keywords: ['turkey', 'istanbul']		},		{			value: 'dxb',			label: 'Dubai International',			description: 'DXB',			keywords: ['uae', 'emirates']		}	];</script>​<Combobox	class="w-80 max-w-full"	label="Airport"	placeholder="Try “turkey” or “uae”"	options={AIRPORTS}	bind:value/>

Async

load is called instead of filtering, debounced, with an AbortSignal — so a slow reply for "ir" can never overwrite the answer for "iraq".

<script lang="ts">	import { Combobox } from 'omaris';​	let value = $state('');​	const PEOPLE = [		{ value: 'ahmed', label: 'Ahmed' },		{ value: 'amina', label: 'Amina' },		{ value: 'dilan', label: 'Dilan' },		{ value: 'hevi', label: 'Hevi' },		{ value: 'karwan', label: 'Karwan' },		{ value: 'layla', label: 'Layla' },		{ value: 'omer', label: 'Omer' },		{ value: 'rania', label: 'Rania' },		{ value: 'zainab', label: 'Zainab' }	];​	async function search(query: string, signal: AbortSignal) {		// Stand-in for a real request; the signal is what makes it cancellable.		await new Promise((resolve) => setTimeout(resolve, 400));		signal.throwIfAborted();		return PEOPLE.filter((person) => person.label.toLowerCase().includes(query.toLowerCase()));	}</script>​<Combobox	class="w-72 max-w-full"	label="Assign to"	placeholder="Start typing a name"	load={search}	minChars={1}	bind:value/>

Allow custom

allowCustom takes whatever is typed when nothing matches — tags, labels.

Nothing chosen yet.

<script lang="ts">	import { Combobox } from 'omaris';​	let tag = $state('');​	const TAGS = [		{ value: 'bug', label: 'bug' },		{ value: 'docs', label: 'docs' },		{ value: 'design', label: 'design' }	];</script>​<Combobox	class="w-72 max-w-full"	label="Tag"	placeholder="Pick one or invent one"	options={TAGS}	allowCustom	bind:value={tag}	supportingText={tag ? `Value: ${tag}` : 'Nothing chosen yet.'}/>

Overridden

Combobox is an Input frame with the shared Listbox under it, so it takes both sets of parts: control is the text field, option a row of the popup.

<script lang="ts">	import { Combobox } from 'omaris';​	const CODES = [		{ value: 'iq', label: 'IQ +964' },		{ value: 'tr', label: 'TR +90' },		{ value: 'ae', label: 'AE +971' }	];</script>​<Combobox	class="w-64 max-w-full"	label="Dial code"	options={CODES}	value="iq"	classes={{ control: 'font-mono', option: 'font-mono' }}/>

When to use it

Use it for

  • One value from a list too long to scroll: a country, a person, a product, a city. A few typed letters cut the list to three rows.
  • Search against a server. Pass load; it is debounced and gets an AbortSignal, so a slow reply for "ir" never lands over "iraq".
  • Free text with suggestions, like tags or labels. Set allowCustom.
  • Matching on text that is not in the label. keywords lets "uae" find the Emirates and an ISO code find its country.

Not for

  • A short, fixed list nobody would search → Select.
  • Finding a page, an order or a record anywhere in the app → Search Bar. It opens a results view instead of filling a field.
  • Several values at once. Pick them one at a time and show each as an input Chip with onremove. The box holds one value.
  • Actions matched by name → Menu. Typing a letter already jumps to the row there.

Do

  • Use bind:selected when you render more than the value next: the label, the description, the avatar.
  • Set minChars={2} with load against a large index, and openOnFocus={false} so an empty query does not open an empty list.
  • Write an emptyText that says what to do next: "No one by that name — invite them", not "No results".
  • Leave highlight on. The bold run shows why a row matched.

Don't

  • Set allowCustom where the value must exist, like an assignee or a customer id. A typo becomes a record.
  • Filter thousands of options locally. maxResults caps what is drawn, not what is matched. Past a few hundred rows use load.
  • Turn off clearable on an optional field. The × is how the person says "none" without emptying the text by hand.

API

Combobox

Combobox — type to search, arrow to the one you want, Enter to take it.

Give it options and it filters them as you type (accent-insensitive, matches anywhere in the label or the option's keywords). Give it load instead and it asks you for results, debounced, with an AbortSignal so a slow reply for "ir" never overwrites the answer for "iraq":

Focus never leaves the input — the list is driven with aria-activedescendant — so it works with a screen reader, a phone keyboard and a form's Tab order exactly like a plain text field.

import { Combobox } from 'omaris'
<Combobox bind:value options={countries} label="Country" /><Combobox bind:value load={(q, signal) => api.users(q, { signal })} />

Parts

Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.

root
Wrapper: label, field, supporting text.
field
The frame: border, background, height, adornment layout.
control
The bare <input>; the frame owns every decoration.
label
No description in the source yet.
adornment
Icon slots at either end of the frame.
affix
Flat text glued to the value — "https://", "kg".
action
An interactive trailing control: reveal, clear.
support
Helper line under the field, and the counter beside it.
counter
No description in the source yet.
surface
No description in the source yet.
option
No description in the source yet.
group
A non-interactive heading above a run of options.
body
No description in the source yet.
description
No description in the source yet.
check
No description in the source yet.
empty
"No results", "Searching…".
highlight
The part of a label that matched what was typed.

Props

variant

Defaults to 'outline'

InputVariant
size

Defaults to 'default'

InputSize
value bindable

Defaults to ''

string

The chosen option's value. Bindable.

selected bindable

Defaults to null

ComboboxOption | null

The chosen option itself. Bindable, for when value alone isn't enough.

query bindable

Defaults to ''

string

What's in the box. Bindable.

options
ComboboxOption[]

A fixed list, filtered locally.

load
ComboboxLoad

Fetch results for a query instead.

debounce

Defaults to 200

number

ms to wait after the last keystroke before calling load.

minChars

Defaults to 0

number

Don't call load until this many characters are typed.

maxResults

Defaults to 50

number

Cap on rows shown from a local filter.

filter
(option: ComboboxOption, query: string) => boolean

Your own local matcher.

allowCustom

Defaults to false

boolean

Take whatever's typed as the value when nothing matches.

openOnFocus

Defaults to true

boolean

Open the list as soon as the field is focused.

clearable

Defaults to true

boolean

Show an × that empties the field.

highlight

Defaults to true

boolean

Bolden the matched part of each label.

emptyText

Defaults to 'No results'

string
loadingText

Defaults to 'Searching…'

string
label
string
supportingText
string
invalid
boolean

Inside a Field, defaults to the field's.

required
boolean

Inside a Field, defaults to the field's.

start
Snippet

Leading icon inside the frame.

option
Snippet<[ComboboxOption, { selected: boolean; active: boolean; query: string }]>

Custom row rendering.

onchange
(value: string, option: ComboboxOption | null) => void
class
string
classes
ComboboxClasses

Per-part Tailwind overrides. class still covers the root.

ref bindable

Defaults to null

HTMLInputElement | null