Inputs
Combobox
Combobox — type to search, arrow to the one you want, Enter to take it.
import { Combobox } from 'omaris' 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 anAbortSignal, 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.
keywordslets "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:selectedwhen you render more than the value next: the label, the description, the avatar. - Set
minChars={2}withloadagainst a large index, andopenOnFocus={false}so an empty query does not open an empty list. - Write an
emptyTextthat says what to do next: "No one by that name — invite them", not "No results". - Leave
highlighton. The bold run shows why a row matched.
Don't
- Set
allowCustomwhere the value must exist, like an assignee or a customer id. A typo becomes a record. - Filter thousands of
optionslocally.maxResultscaps what is drawn, not what is matched. Past a few hundred rows useload. - Turn off
clearableon 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 bindableDefaults to ''
string The chosen option's value. Bindable.
selected bindableDefaults to null
ComboboxOption | null The chosen option itself. Bindable, for when value alone isn't enough.
query bindableDefaults 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 bindableDefaults to null
HTMLInputElement | null