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…
Live specimen
Interact with the demo. Every part is real and numbered.
Anatomy: every part, named
Hover a row to locate it in the demo above.
Build prompt
How it behaves right-to-left
This section is ours alone.
Popup alignment and the top layer
mirrorsThe 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 mirrorsWhere 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 mirrorsMarking 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 mirrorsMatching 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 mirrorsThe 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.
See also
Updated · 2026-08-19