UIDex

Combobox / Autocomplete

حقل الإكمال التلقائي

role="combobox"

also calledautocomplete، typeahead، autosuggest، searchable select، filterable dropdown، lookup field، كومبوبوكس، أوتوكومبليت، الإكمال التلقائي، حقل بحث بقائمة، قائمة منسدلة قابلة للكتابة، حقل اقتراحات

A combobox is an editable text field wired to a list of options that narrows with every keystroke, and it ends with the user picking one member of a set the interface already knows. Three other controls look the same on screen. A plain select has no editable text, since typing into it only jumps to the nearest matching option and the list never filters. The role will not separate the two either, because HTML maps a one-line <select> onto the same combobox role that ARIA 1.2 gives the hand-built version; what separates them is an <input> the user can edit and an aria-autocomplete value that says how completion works. A search field takes free text and submits it as a query, so what comes out is a sentence the user wrote rather than the id of a row, and text matching nothing is still a valid entry. A menu is the third: role="menuitem" children fire commands and hold no value, and a command palette belongs with it despite wearing the same combobox and listbox roles, because Enter there runs an action instead of writing a value. What stays in the field after Enter settles it: an option id means a combobox, the words the user typed mean a search field, and an emptied field with something now running means a command.

If you called it…

"the box that suggests things while you type""a dropdown you can type into""the field that finds the country after two letters""the list that gets shorter as I type""the search box with options under it"

Live specimen

Interact with the demo. Every part is real and numbered.

مطار الوصول٣ نتائج
الدوحةDOH
دبيDXB
جدةJED
RTLاللائحة تلتصق ببداية الحقل، والرمز اللاتيني معزول بـ bdi

Anatomy: every part, named

Hover a row to locate it in the demo above.

Build prompt

Build a combobox that picks one member of a known set, not a free-text search field. The control is an <input type="text"> that stays editable at all times and carries role="combobox", aria-autocomplete="list", a genuinely toggling aria-expanded, an aria-controls pointing at the listbox id, and an aria-labelledby tying it to the field label; never put readonly on it. The popup is role="listbox" with role="option" children, each holding an id derived from the data key and an explicit aria-selected, true on the committed value only. Keep focus in the field and put aria-activedescendant on the input itself, calling scrollIntoView({ block: "nearest" }) as the active row moves. Support ArrowDown, ArrowUp, Enter and Escape (close and keep the text, a second press clears it), and leave ArrowLeft, ArrowRight, Home and End to the caret. Debounce the request by 250ms, cancel the previous one with AbortController, and drop any response that does not belong to the latest query. Put an empty aria-live="polite" region in the page from first paint, outside the listbox, to announce the result count, render the "no results" message as ordinary text rather than a role="option" row, and keep everything except role="option" children out of the role="listbox" element. Fold the text before matching: strip tashkeel and tatweel, map the hamza forms onto ا, convert Arabic-Indic digits to ASCII, and compute the highlight offsets against the rendered string. Use logical properties throughout: inset-inline-start for popup alignment, inset-inline-end for the spinner and the clear button, padding-inline for spacing, text-align: start for text, and never left or right. If you portal the popup, set dir on the portal container, do not mirror the spinner in RTL, and keep the match highlight to a background colour without touching font weight.

How it behaves right-to-left

This section is ours alone.

Popup alignment and the top layer

mirrors

The popup hangs off the logical start edge of the field, its right in Arabic, so write inset-inline-start: 0 inside a position: relative wrapper. The trap is how you lift it above the rest of the page: createPortal to document.body moves the node out of your subtree, dir="rtl" stops being inherited, the text snaps back to left alignment, and Floating UI reads getComputedStyle(...).direction off the floating element itself, measures ltr, and puts the -start alignment on the wrong side. Set dir on the portal container, or on <html> so nothing can escape it. The clean way out is the popover attribute together with CSS anchor positioning: popover promotes the box to the top layer at paint time while it keeps its place in the tree, so dir is still inherited, and anchor positioning lines that box up with a field sitting anywhere in the document, with position-area taking logical values such as block-end span-inline-end.

The loading spinner

never mirrors

Where the spinner sits mirrors; how it is drawn does not. Pin it with inset-inline-end so it moves to the left of the field in Arabic, and keep scaleX(-1) off it and off every ancestor: flipping the axis reverses the rotation, so it spins anticlockwise and reads as broken. A blanket [dir="rtl"] svg { transform: scaleX(-1) } is the usual cause, so scope the flip to a class you put on arrows only. A spinner painted with conic-gradient ignores direction completely, since conic angles sweep clockwise from the top and gradients have no logical form to begin with, though the same stray scaleX would still reverse it.

Highlighting inside a word

never mirrors

Marking a run inside an Arabic word splits it into separate inline boxes, and that alone does not break the cursive join, because a browser shapes across an element boundary as long as the font properties match and no inline-axis margin, border or padding sits between the two sides. Give the marked run font-weight: 700 and a second face gets pulled in, the join is cut, and الدوحة comes apart at the match; inline letter-spacing does the same. Keep the highlight to background-color and color. Never wrap the run in <bdi> or set unicode-bidi: isolate on it, because an isolate is a direction boundary that severs the join and turns the fragment into its own bidi run, which can be reordered away from the rest of the word.

An Arabic query over Latin labels

never mirrors

Matching fails in Arabic before direction is even involved: "احمد" does not match "أحمد", because includes compares code points. Intl.Collator does flatten أ and إ onto ا at sensitivity "base", but it leaves آ and ٱ distinct, and it compares whole strings anyway with no way to search inside one. Fold both sides before comparing: strip tashkeel by removing U+064B through U+0652, drop the tatweel U+0640, map أ إ آ ٱ to ا and ى to ي, convert Arabic-Indic and Persian digits to ASCII, and lowercase the Latin side. Wrap a Latin code sitting in a mixed row in <bdi>, or the digits and punctuation beside it drag it to the far end of the line.

The caret and its keys

never mirrors

The field owns the caret, so ArrowLeft and ArrowRight stay bound to character movement and never drive the list, and no direction branch is needed anywhere. Home and End go to the start and end of the text here, not to the first and last option, which is the reverse of a bare listbox. selectionStart and selectionEnd are logical offsets into the string and do not change with page direction, but in mixed text the visual caret does not advance in step with them, so never derive its x position by measuring the width of the substring before it. When one field has to hold either Arabic or a Latin code, dir="auto" on the input is the fix: the browser reads the first strong character of the value and sets the field direction from it, stepping over digits and punctuation to find one.

In code

Each row is one framework's word for the same thing. Take the row your project speaks.

ARIArole="combobox" + aria-autocomplete="list"list means typing only filters the popup, both adds an inline completion in the field on top of it, and inline completes with no popup at all.
ARIAaria-activedescendantIt goes on the field, not on the list, because focus stays in the field even while the arrows move.
HTML<input list="…"> + <datalist>The cheapest correct version, but the browser owns the popup and the match rule and leaves you no styling hook for either.
shadcn/ui<Command> inside <Popover>There is no ready-made Combobox in the library; you assemble one from cmdk inside a Popover, replacing the matcher through shouldFilter and filter.
Material UI<Autocomplete filterOptions freeSolo>freeSolo allows a value that was never in the list, which turns it into a search field, and filterOptions is where the Arabic folding belongs.
Ant Design<AutoComplete> vs <Select showSearch>The first accepts whatever the user typed as the final value; the second only ever emits the key of an option that exists.
CSSunicode-bidi: isolateIsolates a Latin code inside an Arabic label, and must never be put on a highlighted run inside a word.

See also

Updated · 2026-08-19