UIDex

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…

"the box with an arrow that opens a list""that country picker thing""the list that drops down when you click it""the field where you choose instead of typing""the box where you pick one of the options"

Live specimen

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

المدينة
القاهرة
الرياض
دبي
عمّان
RTLاللائحة تُحاذى ببداية المشغّل، والسهم لا ينعكس

Anatomy: every part, named

Hover a row to locate it in the demo above.

Build prompt

Build a value-holding dropdown (a select), not a command menu. The trigger is a button showing the current value with role="combobox", aria-haspopup="listbox", a genuinely toggling aria-expanded, an aria-controls pointing at the list id, and aria-labelledby tying it to the field label. The popup is role="listbox" whose children are role="option", each with a stable id and an explicit aria-selected, exactly one of them true. Keep focus on the trigger and track the highlighted row with aria-activedescendant, calling scrollIntoView({ block: "nearest" }) on every move. Support ArrowUp, ArrowDown, Home, End, Enter, Escape (close and restore focus to the trigger), and first-letter type-ahead. Make every position and spacing rule logical: inset-inline-start for alignment, padding-inline-start for the checkmark gutter, and text-align: start inside options. Do not mirror the chevron in RTL: it points down and should only rotate 180deg when open. Add a hidden input with a name so the choice reaches the form, and flip the popup upward automatically when there is no room below.

How it behaves right-to-left

This section is ours alone.

Popup alignment

mirrors

The 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 mirrors

The 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

mirrors

Option 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 mirrors

ArrowUp, 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

mirrors

If 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.

HTML<select><option value>The native element: full accessibility for free and an OS picker on mobile, at the cost of unstylable options.
ARIArole="listbox" + aria-selectedThe role that makes the list hold a value; without it you have a command menu.
ARIArole="combobox" + aria-expanded + aria-controlsAll three go on the trigger itself, even in the non-typeable variant.
Radix UI<Select.Root> vs <DropdownMenu.Root>The library splits them on purpose: one takes value and onValueChange, the other takes onSelect.
shadcn/ui<Select> / <Command> comboboxSelect wraps Radix, while the searchable combobox is assembled from Command inside a Popover.
Material UI<Select> vs <Autocomplete>Autocomplete is the real typeahead combobox; Select picks from a fixed list.
CSSinset-inline-startThe logical alignment that keeps the popup on the trigger's start edge in both directions.

See also

Updated · 2026-08-19