@akhatmirzo/ui

Combobox

A searchable select with Uzbek-aware search, multiple selection, creatable options and server search.

Hali tanlanmagan

Import

import { Combobox, type ComboboxOption } from '@akhatmirzo/ui';

When to use

  • Long lists — cities, districts, customers, products. Scanning more than 10–15 options by eye is slow; typing is faster.
  • Several values — multiple: tags, branches, assignees.
  • Values that may not exist yet — creatable (a new tag, a new category).
  • Data that lives on the server — onSearch and loading send every query to your API.
  • A short list with no need for search — a plain Select is enough.

Search is powered by searchMatch from @akhatmirzo/core, and it forgives the usual quirks of written Uzbek:

  • Every apostrophe is the same: gʻ, g', g‘ and g’ all match. Leaving the apostrophe out works too: ulugbek finds "Mirzo Ulug'bek".
  • Cyrillic ↔ Latin: typing Чилонзор finds "Chilonzor", and Тошкент finds "Toshkent".
  • Case-insensitive: TOSHKENT and toshkent are the same query.
  • Matches inside words: chirchiq finds "Oʻrta Chirchiq".
  • The matched part is bold — in the description line as well.

Yozib ko'ring: guzor, g‘allaorol, Чилонзор, ulugbek

Use it in your own search

The same helpers are exported on their own: searchMatch, normalizeUz and splitByRanges — see core/text. Normalizing queries with normalizeUz helps on the server side too.

Multiple selection

With multiple, value becomes an array (string[]). Selected options show up as Badge chips inside the trigger, each with its own ✕. The panel stays open after each pick; Backspace on an empty query removes the last selection.

2 ta tanlangan

ToshkentSamarqand

Creating options

With creatable on, a "«text» yaratish" item appears at the end of the list whenever the query doesn't exactly match an existing option. If onCreate returns an option, that option gets selected; if it returns nothing, the text itself is used as both value and label. Adding the new option to options is up to you.

Ro'yxatda yo'q tegni yozing va Enter bosing

frontend

Passing onSearch turns off local filtering: you receive the query (debounced by 300ms) and provide fresh options. While you wait, loading shows a spinner in place of the list. When the panel closes, onSearch('') is called — a good moment to restore the initial list.

The selection survives new results

Even if the selected option is missing from the latest server results, Combobox remembers its label and keeps showing it in the trigger.

Descriptions and icons

ComboboxOption supports description (a second line), icon and disabled. Search looks at the description too. Remove the search icon in the trigger with leftIcon={null}, or pass your own.

interface ComboboxOption {
  value: string;
  label: string;
  description?: string;
  icon?: ReactNode;
  disabled?: boolean;
}

Sizes

The same as Input and Select: 32, 40 and 48px. With multiple, the height grows with the number of chips.

States

Inside a Field, disabled, error and required are wired up for you. loading shows a spinner instead of the list when the panel opens.

Shaharni tanlang

Inside a dialog

Inside a Dialog or a Drawer, the Combobox panel moves to the modal layer and opens above the dialog — nothing to configure.

Props

Combobox

Any other <button> attributes (id, aria-label, aria-*, data-*, onBlur ...) and ref are passed to the trigger.

PropTypeDefaultDescription
optionsrequiredComboboxOption[]—The options
multiplebooleanfalseMultiple selection: value becomes string[]
valuestring | null · string[]—Selected value (an array with multiple)
defaultValuestring | null · string[]—Initial value (uncontrolled)
onValueChange(value: string | null) => void · (value: string[]) => void—Called on change. Clearing gives null (or [])
placeholderstring'Tanlang'Shown while nothing is selected
searchPlaceholderstring'Qidirish...'Placeholder of the search input
emptyTextReactNode'Hech narsa topilmadi'Shown when nothing matches
onSearch(query: string) => void—Server search: 300ms debounce, local filtering off
loadingbooleanfalseSpinner in place of the list
creatablebooleanfalseAdds a "«text» yaratish" item
onCreate(text: string) => ComboboxOption | void—Called on create. A returned option gets selected
clearablebooleanfalseShows a ✕ (clear) button when there is a value
leftIconReactNodesearch iconIcon at the start of the trigger; null hides it
size'sm' | 'md' | 'lg''md'Height: 32 / 40 / 48px
invalidboolean—Error state (automatic inside Field)
disabledboolean—Disables the Combobox
requiredboolean—Required field (aria-required)
namestring—For forms: each selected value is sent as a hidden <input>
openboolean—Whether the panel is open (controlled)
defaultOpenbooleanfalseOpen on first render
onOpenChange(open: boolean) => void—Called when the panel opens or closes
labelsComboboxLabels—Screen reader texts (see below)
contentClassNamestring—Class for the panel

ComboboxLabels

The built-in texts are in Uzbek. Override them for an English (or any other) interface.

PropTypeDefaultDescription
searchstring'Qidirish'Search input name
liststring'Variantlar'List name
loadingstring'Yuklanmoqda'Spinner text
clearstring'Tozalash'Clear (✕) button name
remove(label: string) => string`Olib tashlash: ${label}`Name of the ✕ on each chip
create(text: string) => ReactNode`«${text}» yaratish`Text of the create item
<Combobox
  options={cities}
  placeholder="Choose a city"
  searchPlaceholder="Search..."
  emptyText="Nothing found"
  labels={{
    search: 'Search',
    list: 'Cities',
    loading: 'Loading',
    clear: 'Clear',
    remove: (label) => `Remove ${label}`,
    create: (text) => `Create "${text}"`,
  }}
/>

Data attributes

AttributeOnValue
data-sizecontainersm · md · lg
data-statecontaineropen · closed
data-invalidcontainerpresent in the error state
data-disabledcontainerpresent when disabled
data-placeholdertriggerpresent while nothing is chosen
data-checkedoptionpresent on selected options

Keyboard

KeyAction
Enter / Space / ↓ / ↑Opens the panel when the trigger is focused
A letter or digitOn a closed trigger — opens the panel and starts the search
TypingIn the open panel — searches
↓ / ↑Moves between options (wraps around)
Home / EndFirst / last option
EnterSelects. Single mode closes the panel; multiple keeps it open
EscClears the query first; a second press closes the panel
Backspacemultiple: removes the last selection when the query is empty
TabCloses the panel and moves focus to the next element

Accessibility

  • Roles. The trigger is role="combobox" with aria-haspopup="listbox"; while open, aria-controls points to the list. The list is a listbox (aria-multiselectable="true" with multiple), and every option has aria-checked.
  • Name. Inside a Field, the label and the selected value are read together ("Shahar, Toshkent"). With multiple, the selected labels are added as visually hidden text.
  • Field wiring. description and error go to aria-describedby; errors set aria-invalid="true" and required sets aria-required="true".
  • The ✕ on each chip gets its own name ("Olib tashlash: Toshkent"), configurable through labels.remove.
  • Focus. Opening moves focus to the search input; closing returns it to the trigger (or to the next element when closed with Tab).
  • Icons (leftIcon, option icon) are aria-hidden.

On this page