Skip to content

RTL select and dropdown components

Fix RTL selects and dropdowns by checking native direction, custom popup alignment, chevron spacing, Arabic search and keyboard interaction.

By , Founder of Ritla

Guide14 min read

A dropdown can look Arabic while closed and break as soon as it opens. The selected label aligns correctly, but the popup opens from the wrong edge, its search field starts left to right, or a long option runs underneath the chevron. Fixing the closed control does not fix the component.

Start by identifying what you have: a native select, a custom selection widget, or a menu of actions. They can share a downward arrow while needing different semantics, positioning and keyboard behavior. Applying one “RTL dropdown” stylesheet to all three hides those differences.

This guide uses a ferry reservation's boarding-terminal selector. A small terminal list can stay native. A larger searchable list may justify a custom combobox. Both must preserve the terminal's identity when the displayed name changes language, and both need testing after the options open.

Fix native direction before replacing the select#

A native select generally inherits direction from its HTML ancestors. Give an Arabic form semantic direction, then look for local attributes or CSS declarations that contradict it. MDN's dir reference explains inheritance and why declaring Arabic language alone does not establish RTL direction.

html
<form id="terminal-reservation" lang="ar" dir="rtl">  <label for="boarding-terminal">محطة الصعود</label>  <select id="boarding-terminal" name="boardingTerminal" required>    <option value="">اختر محطة الصعود</option>    <option value="waterfront">محطة الواجهة البحرية</option>    <option value="island">محطة الجزيرة</option>    <option value="old-harbour">محطة المرفأ القديم</option>  </select></form>

The Arabic labels describe the choices; the stable value strings identify them to the application. They do not need Arabic translation. Do not replace those values with translated names and accidentally change the request contract.

This reset would contradict the form:

css
/* Problem: alignment conceals a forced LTR control. */#boarding-terminal {  direction: ltr;  text-align: right;}

Remove the forced direction rather than compensating with alignment. Where alignment styling is needed, text-align: start follows the control's direction; MDN distinguishes that behavior in its text-align reference. Alignment moves text within a box. It does not establish the box's text direction or define native picker behavior.

Inspect the computed direction on the select, not just its parent. Then open it on each supported platform. For the Arabic design, check that the selected text begins at the right and the disclosure affordance leaves it room, typically at the opposite edge. Exact native arrow placement and the expanded picker's presentation depend on the browser, operating system and styling mode. Do not promise identical rendering everywhere.

The supplied layout and bidi measurements used Chromium 151 on September 24, 2026. They do not establish Firefox, Safari or every operating system's native picker appearance.

Keep native selection when it meets the requirement#

For a short list of boarding terminals, replacing a native select solely to move its arrow creates more work than it resolves. Check whether a CSS reset, an explicit dir="ltr" or a custom decoration caused the problem first.

Native control styling has limits. MDN's select documentation distinguishes traditional styling from newer customization features. Treat the closed control and the opened picker as separate surfaces during review. A font or width declaration that changes the first is not proof that the second accepts the same styling.

Review option groups as content too. If terminals are grouped by coast or service area, translate the group labels rather than only the individual options. Native optgroup provides a label for a group; that heading should describe the choices beneath it in the interface language. Check that the placeholder remains an instruction, rather than becoming a selectable terminal with an unintended nonempty value. For an unavailable terminal, test the disabled state in the actual picker instead of merely reducing its text contrast with CSS. The reservation must not accept a choice that the interface presents as unavailable.

If you use appearance: none, you take responsibility for the disclosure affordance that native styling previously supplied. MDN's appearance reference describes this control over platform appearance. Check for a missing arrow, a duplicated arrow, lost focus indication and unreadable disabled states after the change.

Newer customizable select features offer another route, but verify current support and fallback behavior before depending on them. Do not assume richer option markup works in a traditional native select because it works inside a custom listbox.

A long list, remote search or richer result rows can justify a custom component. Those are product requirements. A cosmetic mismatch should first receive a diagnosis of the actual native control.

Choose the interaction before choosing the arrow#

“Dropdown” describes a visual shape, not a complete interaction contract.

Product taskSuitable starting pointWhat the user is doing
Choose one boarding terminal from a short listNative selectSelecting a form value
Choose a terminal through a custom non-editable controlSelect-only comboboxExploring and committing a choice
Type a terminal name and inspect matchesEditable comboboxEditing a query and selecting a result
Open actions for an existing reservationMenu buttonRunning commands or following links

The WAI-ARIA combobox pattern covers editable and select-only variants. A menu button opens a different kind of widget. Do not assign role="menu" to a terminal picker merely because a CSS class calls its popup a menu.

For a custom selection widget, adopt an implemented, tested pattern rather than adding roles to a clickable collection of elements. ARIA identifies relationships and states; the application still needs to manage interaction.

If each proposed terminal row contains a separate timetable link or favourite button, reconsider the pattern. A listbox option is a choice, not a container for independently operable controls. W3C's listbox guidance explains that limitation. Move those actions outside the options or choose a pattern that supports them.

Give the label and chevron separate space#

For a custom trigger, keep the selected label and disclosure icon as separate layout items. Arabic text should not need to flow around a decoration positioned on top of it.

These styles apply to the trigger, its label span and its decorative chevron in an existing custom component. They address presentation, not its opening or keyboard logic:

css
.terminal-trigger {  display: flex;  align-items: center;  gap: 0.75rem;  inline-size: 100%;  box-sizing: border-box;  padding: 0.8rem 1rem;  text-align: start;} .terminal-trigger__label {  flex: 1;  min-inline-size: 0;  overflow-wrap: anywhere;} .terminal-trigger__chevron {  flex: none;}

With the label before the chevron in DOM order and RTL direction on the component, the ordinary flex row places the label toward the right and the chevron toward the left. Keep the source order. Adding row-reverse on top of RTL applies another reversal and changes that result. MDN's flex-direction reference explains how row direction depends on the container's direction.

Let the label wrap if identifying the terminal requires its full name. If the design truncates the closed value, keep the complete option available when the picker opens. Test names that differ at their ends: two truncated labels that become indistinguishable are a selection problem, even if neither overlaps the icon.

A downward disclosure chevron describes opening, not horizontal travel. It does not need horizontal mirroring. Never mirror the entire trigger with a transform to move one icon; that also transforms text and other content. Keep decorative icons out of the accessible name, and check that the label still identifies the selected terminal without them.

Anchor the popup to the intended edge#

Text direction and popup alignment are different decisions. The ferry design can specify that the popup's inline start aligns with the trigger's inline start. In a horizontal RTL interface, that means matching their right edges.

For an absolutely positioned popup that remains inside its positioned wrapper, this physical anchor fixes the left edge in both languages:

css
/* Problem: this always chooses the physical left edge. */.terminal-picker {  position: relative;} .terminal-popup {  position: absolute;  top: 100%;  left: 0;  inline-size: 100%;}

For the same component and intended start-edge alignment, replace that popup rule with:

css
.terminal-popup {  position: absolute;  inset-block-start: 100%;  inset-inline-start: 0;  inline-size: 100%;  box-sizing: border-box;}

MDN documents the direction-sensitive mapping of inset-inline-start. In horizontal LTR layout, this preserves the original left anchor. In RTL, it uses the right anchor. Remove the old physical declarations when applying the replacement; leaving competing offsets makes the outcome harder to diagnose.

Equal-width panels can conceal a wrong alignment setting because their left and right edges already coincide with the trigger's. Temporarily make the popup wider than the trigger to distinguish start alignment from end alignment. Then restore the product's width policy and test it at the viewport boundary.

This local CSS does not solve collision detection. A popup can be correctly start-aligned and still need to move or open above when there is insufficient room. Check the intended anchor separately from the fallback placement.

Check portalled content at its actual DOM location#

A popup may be rendered under body or a dedicated overlay root to escape clipping. That can separate it from the element supplying the trigger's direction, font or component-scoped styles.

Consider an Arabic reservation form embedded inside an otherwise English shell. Its trigger inherits RTL from the form. A popup inserted under the shell's overlay root does not inherit from that form, because it is no longer its DOM descendant. Inspect the rendered popup's ancestors and computed direction rather than assuming the component tree determines CSS inheritance.

React's createPortal documentation makes the distinction useful here: a portal changes physical DOM placement while preserving React relationships such as context. A direction context in a component library can still be available, but the library must apply the relevant direction to the rendered overlay. Context existing in memory does not by itself create a DOM dir attribute.

Give the popup the intended direction through the library's supported API or its rendered root. Carry language as well if the overlay escapes the Arabic language boundary. Check loading messages, group headings and the search field too; correcting only option labels can leave the surrounding popup inconsistent.

If the document root is already RTL, a popup under body may inherit the desired direction without an extra declaration. The defect depends on where the direction boundary lives. Do not add blanket overrides to every overlay before inspecting that boundary.

A related failure occurs when selectors depend on a wrapper ancestor. A rule such as .terminal-picker .terminal-popup cannot match a popup outside that wrapper. Use the library's popup class or supported styling mechanism instead of increasing specificity on a selector whose relationship no longer exists.

Distinguish logical placement from calculated coordinates#

Positioning libraries can express a logical alignment while writing physical coordinates to the element. Those coordinates are not automatically evidence of an RTL defect.

For example, Floating UI documents bottom-start and bottom-end as direction-aware alignments in its computePosition reference. The resulting x and y coordinates are the output of that positioning calculation. Replacing the library's generated left coordinate with an inline-start property can change the coordinate's meaning and break placement.

Configure direction and placement at the library's supported boundary. Confirm which element supplies direction for its calculations, especially with a portalled popup. Then inspect the result with a popup wider than its trigger and with the trigger near each viewport edge.

Keep the placement calculation current when the form scrolls, the viewport changes or result rows resize. Use the library's documented update lifecycle and release its observers when the popup closes. A correctly aligned popup that remains behind after its trigger moves is still broken.

Collision handling can legitimately change the final alignment. Record both the requested placement and the resolved placement when debugging, instead of treating every shifted panel as an inheritance failure. Also test clipping inside a dialog or scroll container, where a large z-index alone does not establish that the popup can escape its clipping boundary.

Isolate terminal references within Arabic option labels#

Custom options can combine an Arabic name with a reference used by operations staff. Treat the reference as a value with its own direction, not as a reason to make the entire option LTR.

For an existing custom option renderer, the text content can be:

html
<span class="terminal-option__name">محطة الواجهة البحرية</span><bdi class="terminal-option__reference" dir="ltr">738-KP</bdi>

The surrounding option retains its Arabic direction. The reference stays a separate LTR run. Without isolation, a digits-then-Latin reference after Arabic text matches the measured reordering case: its groups can appear as KP-738. That matters because the groups identify a terminal, rather than describing a range someone should read from the right.

Do not reverse the stored reference to compensate. Selection, search and submission must still use its logical value. A copy-and-paste check alone cannot prove the visual order is correct, so compare the displayed reference with its intended grouping too. Bidi isolation explains the boundary in more detail.

This fragment is custom option content, not a recommendation to put nested elements into a traditional native option. For native controls, verify the supported content model and actual picker rendering. Keep the short Arabic label when that is enough to identify the terminal.

Make search work without turning editing into navigation#

An editable terminal combobox needs a labelled input, translated search states and a defined relationship between query text and committed selection. It also needs a direction policy for the query.

Use RTL for an input intended for Arabic terminal names. A dedicated reference-code search can use LTR. If one input accepts both and uses dir="auto", test its empty state, Arabic typing and code typing separately. In the supplied measurements, an empty automatic-direction input resolved LTR, including placement of an Arabic placeholder. A translated placeholder therefore does not prove the editing field starts with the desired direction.

Search matching is a different decision from input direction. Specify whether the terminal search ignores Arabic diacritics, accepts spelling variants or matches references. Use agreed examples to test that policy rather than deleting Arabic characters until a Latin-oriented search helper happens to match. Preserve the selected terminal's ID even when the query or matching representation is normalized.

Translate the empty-result and loading states as well as the placeholder. For this selector, a no-match message could be لا توجد محطة مطابقة. جرّب اسمًا آخر. A temporary request failure needs different wording and a retry path; it should not claim there are no terminals.

Do not interpret a search query as a confirmed terminal. If the passenger types a different name after selecting one, decide whether that clears the committed value, preserves it with a clear distinction, or requires choosing a new result. The displayed label and submitted ID must not silently describe different terminals.

When results arrive asynchronously, apply them only to the query that requested them. If an older response replaces a newer result list, keyboard focus can jump to an irrelevant terminal even though the search text looks correct.

Test text entry with the keyboards people actually use. Some input methods produce composition sessions rather than a sequence of completed characters. An Enter handler that always accepts the highlighted terminal can commit a result while the person is still composing text. KeyboardEvent.isComposing identifies keyboard events within a composition session; use the component's supported composition handling and verify it on your target input methods. Arabic text does not imply that every Arabic keyboard uses composition, so do not invent one universal event sequence.

Avoid rebuilding the search input after every filtered result. Check that query updates preserve focus and the caret, including editing a word in the middle of an Arabic name. A picker that only works when characters are appended at the end has not passed an editing test.

Keep vertical navigation vertical#

RTL direction is not a reason to swap every arrow key. A vertical option list still has a next item below and a previous item above. Left and Right in an editable input also belong to text editing; intercepting them as selection commands can prevent passengers from correcting a query.

Follow the chosen pattern's documented interaction instead of designing keys from the chevron's position. The W3C select-only combobox example shows one model for exploring options, committing a value and closing without committing. Its examples require testing with the browser and assistive technology combinations you support.

For the ferry selector, write down these observable outcomes:

InteractionWhat to establish
Open with the keyboardThe expanded state matches the visible popup
Move through resultsThe indicated option follows the documented vertical order
Accept a resultThe visible terminal and committed value agree
Dismiss the popupThe documented previous-value policy is preserved
Leave with TabFocus continues through the reservation form
Edit an Arabic queryText editing keys remain usable

If focus stays on the combobox while an option is indicated through aria-activedescendant, update the referenced ID and keep that option visible. MDN's aria-activedescendant reference explains that the attribute communicates the active item; it does not implement scrolling or keyboard behavior.

Filtering and virtualization make that requirement easy to miss. An active ID must not point to an option removed from the rendered result set. After filtering, establish the new active item or clear the reference according to the interaction model. Then test navigation beyond the initially visible results.

Keep terminal identity separate from result position. Suppose the waterfront terminal is the second result before filtering and the first afterward. If selection is stored as a result index, the same number can now identify a different terminal. Store the selected terminal's stable ID and derive its current row from that ID.

Apply the distinction to rendered option IDs and locale changes as well. A translated name is display content, so changing it should not accidentally transfer the active state to another terminal. To reproduce this class of defect, highlight a result, narrow the search until the list changes order, then accept it and inspect the committed ID. Repeat after switching language. This checks the connection between the visible option and the stored selection, which a closed-state screenshot cannot establish.

Verify selection after opening, filtering and resetting#

A custom trigger's text is not automatically a submitted form value. Check the library's form integration, including the field name, selected ID, disabled state and reset behavior. Inspect the request or the form's FormData, rather than inferring submission from the label on screen.

Test choosing the waterfront terminal, searching for another terminal, dismissing the popup and submitting. This sequence exposes a stale hidden value or an unintended commit more clearly than selecting once and taking a screenshot.

Switch the interface language after selection. The boarding terminal should remain the same entity while its displayed name changes. Reset the form as well: a reset that clears the stored ID while leaving the selected label produces a control that promises a selection it will not submit.

When a terminal becomes unavailable, make the resulting state explicit. Do not leave an apparently valid label on the trigger while silently deleting its submitted value. Explain the required new choice in Arabic and associate that feedback with the selection control.

Review the open popup at the edges of the interface#

Check the longest actual terminal name in both the closed trigger and an open option. At narrow widths and increased text size, look for text crossing the chevron, clipped option names and a result list extending beyond the visible screen. A popup that technically exists but cannot be reached by touch or keyboard does not provide a usable choice.

Repeat the review inside the real reservation dialog or embedded form. Put the trigger near the bottom and both horizontal edges, open the popup, then scroll its containing panel. Confirm that placement updates and the active result remains visible. On touch devices, test with the search keyboard open, because it changes the space available to an editable picker.

Use a fresh visit for translated loading and error states, not only a cached successful search. Inspect the popup's actual direction, the search input's direction and its active-option relationship independently. A right-aligned label can conceal problems in each of them.

A free Ritla scan can report R072: <select> rendered LTR with Arabic options. Test custom-popup placement and keyboard interaction directly; debugging RTL layout problems is the next step when the popup's direction is correct but its position still is not.

Checks in this guide

See what your Arabic pages are hiding

Paste a URL. Ritla renders the page on desktop and mobile, runs every check, and shows the top issues with screenshot evidence.

Scan a page freeBrowse the checks