Dropdown / Select
القائمة المنسدلة<select> / role="listbox"
also calledselect، select box، picker، option list، listbox، select menu، دروب داون، سيلكت، قائمة الاختيار، اللائحة المنسدلة، حقل الاختيار، منسدلة
A dropdown is a button that shows one chosen value; opening it reveals a list of options that replace that value, then it closes again. Its defining trait is that it holds a value: the choice stays visible on the button after closing, it submits with the form, and something can read it later. Do not confuse it with three look-alikes. The first is a typeable combobox, which puts an editable <input> where the button was, filters the list on every keystroke, and advertises that with aria-autocomplete="list". What separates them is not the role, because ARIA 1.2 puts role="combobox" on both triggers alike; it is whether a text field exists at all, and that shared role is exactly why the two get mixed up. The second is a menu: role="menu" with role="menuitem" children that fire commands like Copy and Delete. It stores nothing, and that is why aria-selected has no place in it. The third is a popover, just a floating box with no selection semantics and no ownership between the container and what sits inside it. The one-line test: if activating a row performs an action it is a menu, and if it changes a value something will read later it is a select. Inside the select family there is a second split. The native <select> is drawn by the operating system and its options barely take styling, but it gives you full accessibility for free. A custom role="listbox" is fully stylable and puts every keyboard and ARIA obligation on you.
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
mirrorsThe popup aligns to the trigger's logical start edge: its right in Arabic, its left in English. Write inset-inline-start: 0 inside a position: relative wrapper instead of left: 0. If you use Floating UI, its -start and -end alignments are already logical and read direction off the element itself, so placement: "bottom-start" needs no direction branch from you. The bigger trap is portalling the list to document.body: there it is cut off from the dir="rtl" it inherited from your Arabic wrapper, so its text snaps back to left alignment, and the positioning library measures direction as ltr and mirrors the alignment the wrong way. Fix it by setting dir on the portal container itself, or on <html> directly, not only on the page wrapper.
The chevron does not mirror
never mirrorsThe dropdown chevron points down, and down has no left or right, so never give it scaleX(-1) or rtl:-scale-x-100 the way you would a breadcrumb chevron. The only transform it needs is rotate(180deg) on open. In a native <select> the browser draws the arrow itself and moves it to the logical end edge once direction: rtl applies. The moment you kill it with appearance: none, your replacement must be pinned with inset-inline-end, not right.
Inside the option row
mirrorsOption text aligns with text-align: start, never left, and the gutter reserved for the checkmark is padding-inline-start. The classic trap is a mixed list such as "الرياض" next to "Riyadh (+966)": wrap every Latin run in <bdi> or a span with unicode-bidi: isolate, or the bidi algorithm will throw the + and the brackets to the wrong end of the line.
Vertical keys
never mirrorsArrowUp, ArrowDown, Home and End are completely untouched by page direction: Home is the first option and End is the last one in Arabic exactly as in English, because the order here is logical in the DOM, not visual on screen. Never add an if (dir === "rtl") branch to swap them. That breaks the user's expectation and contradicts the ARIA listbox pattern. Escape likewise closes and returns focus to the trigger with no relation to direction.
Arrows in a horizontal list
mirrorsIf you build a horizontal variant with aria-orientation="horizontal", ArrowRight and ArrowLeft swap meaning: under RTL, ArrowLeft moves to the next option and ArrowRight to the previous one. Read the direction once from getComputedStyle(el).direction and multiply your index step by -1 when it is rtl, instead of hard-wiring key names to movement all over the handler.
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