Skip to content

RTL dropdown menus: alignment and placement

Build RTL dropdown menus with deliberate trigger alignment, usable submenus, checkmark and shortcut spacing, and direction-aware keyboard access.

By , Founder of Ritla

Guide13 min read

An Arabic dropdown can open at the intended edge while its submenu still behaves like the English version. Its checkmarks may occupy the label's space, its shortcut hints may drift into another column, and the key that opens a branch may disagree with the component's configured direction.

Do not begin by moving every menu to the right. Decide which trigger edge the menu should align with, which direction its hierarchy prefers and how it behaves when there is insufficient space. Reading-start alignment is useful for some menus; an account menu at the edge of a toolbar may need reading-end alignment instead.

This guide uses a field-recording editor's actions menu. It can copy a recording link, open an export submenu and toggle whether exports include notes. That is an application command menu, so its keyboard and checked states belong to a menu interaction model.

Choose the interaction before choosing the alignment#

“Dropdown” describes an appearance, not one semantic component. A form control selecting one recording category, a list of navigation links and an editor's command menu can all open beneath a button, but they have different responsibilities.

Use a native select where a form needs a single selected value and its behavior fits. Use a disclosure for ordinary links if visitors should Tab through them like other website destinations. The WAI disclosure navigation example explains why website navigation does not automatically require an ARIA menu role.

The recording actions are commands with an export branch and a checked option. Here, a menu button and menu items are appropriate. Opening moves focus into the menu, vertical keys navigate its items and activating a command performs the action. The WAI menu button pattern describes that opening relationship.

Do not add menu roles to a generic panel while retaining unrelated disclosure keyboard behavior. Likewise, do not add a second arrow-key handler around a library that already manages menu navigation. Establish which component owns interaction state before fixing its RTL styles.

Align to the trigger's intended edge#

A start-aligned menu follows the trigger's reading-start edge: right in horizontal Arabic, left in English. An end-aligned menu follows the opposite edge. Neither is inherently more Arabic; the correct choice depends on the trigger's location and the product's intended relationship.

Consider an inline, end-aligned variant of the recording menu. Its panel is inside a positioned wrapper around the trigger. The English implementation might use this physical anchor:

Problem

css
.recording-actions__panel {  position: absolute;  top: calc(100% + 0.375rem);  right: 0;}

Keeping right: 0 anchors the same physical edge in Arabic, even when the intended end edge has changed. Express that relationship directly:

Better

css
.recording-actions__panel {  position: absolute;  inset-block-start: calc(100% + 0.375rem);  inset-inline-end: 0;}

This preserves the English right-edge alignment and makes the Arabic variant left-edge aligned. A start-aligned variant would use inline start instead. MDN documents the mapping in its inset-inline-end reference.

This CSS is an alternative for an inline positioned panel, not an override to apply over a positioning library's generated coordinates. The library example below chooses start alignment explicitly. Its placement engine owns the resulting physical offsets.

Also check the containing block. An offset relative to the whole recording toolbar does not become relative to the individual trigger because it uses logical properties. Absolute positioning in RTL covers that distinction.

Configure direction in the command-menu component#

An Arabic DOM boundary can mirror layout while a component's internal keyboard model remains configured for LTR. Pass direction through the component API as well as ensuring the rendered portal content has the intended language and direction.

The following React example assumes react and radix-ui are installed and resolved by the application. The host supplies onCommand(command, options) to perform recording actions. The optional copyShortcut is a display string for a shortcut the host has actually registered; the component does not register one.

jsx
import { useState } from "react";import { DropdownMenu } from "radix-ui"; export function RecordingActions({ onCommand, copyShortcut = null }) {  const [includeNotes, setIncludeNotes] = useState(true);  const run = command => onCommand(command, { includeNotes });   return (    <DropdownMenu.Root dir="rtl">      <DropdownMenu.Trigger type="button" lang="ar" dir="rtl">        إجراءات التسجيل      </DropdownMenu.Trigger>      <DropdownMenu.Portal>        <DropdownMenu.Content className="recording-menu"          lang="ar" dir="rtl" side="bottom" align="start"          sideOffset={6} collisionPadding={8}>          <DropdownMenu.Item className="recording-menu__item"            textValue="نسخ رابط التسجيل"            onSelect={() => run("copy-link")}>            <span className="recording-menu__label">نسخ رابط التسجيل</span>            {copyShortcut && (              <kbd className="recording-menu__hint" dir="ltr"                   aria-hidden="true">{copyShortcut}</kbd>            )}          </DropdownMenu.Item>           <DropdownMenu.Sub>            <DropdownMenu.SubTrigger className="recording-menu__item"              textValue="تصدير التسجيل">              <span className="recording-menu__label">تصدير التسجيل</span>              <span className="recording-menu__hint" dir="ltr"                    aria-hidden="true">‹</span>            </DropdownMenu.SubTrigger>            <DropdownMenu.Portal>              <DropdownMenu.SubContent className="recording-menu"                lang="ar" dir="rtl" sideOffset={6} collisionPadding={8}>                <DropdownMenu.Item className="recording-menu__item"                  textValue="ملف صوتي"                  onSelect={() => run("export-audio")}>                  <span className="recording-menu__label">ملف صوتي</span>                </DropdownMenu.Item>                <DropdownMenu.Item className="recording-menu__item"                  textValue="تفريغ نصي"                  onSelect={() => run("export-transcript")}>                  <span className="recording-menu__label">تفريغ نصي</span>                </DropdownMenu.Item>              </DropdownMenu.SubContent>            </DropdownMenu.Portal>          </DropdownMenu.Sub>           <DropdownMenu.Separator className="recording-menu__separator" />          <DropdownMenu.CheckboxItem className="recording-menu__item"            textValue="تضمين الملاحظات في التصدير"            checked={includeNotes}            onCheckedChange={value => setIncludeNotes(value === true)}>            <DropdownMenu.ItemIndicator className="recording-menu__indicator"              aria-hidden="true">✓</DropdownMenu.ItemIndicator>            <span className="recording-menu__label">              تضمين الملاحظات في التصدير            </span>          </DropdownMenu.CheckboxItem>        </DropdownMenu.Content>      </DropdownMenu.Portal>    </DropdownMenu.Root>  );}

The Radix Dropdown Menu documentation defines these component parts, direction and placement options. The component supplies the menu semantics and focus management; the example does not add competing ARIA roles or click controllers.

The command identifiers stay stable when labels are translated. Copying and exporting are application operations, while the checked option is local component state passed with the command. Persist or control that option through the host if it must survive this component unmounting.

The separate portals make direction at the rendered menu roots explicit. Do not assume that an Arabic wrapper around the trigger remains a DOM ancestor of either menu. Check inherited typography too: a portal can preserve framework context without preserving the same HTML ancestors or scoped style selectors.

Reserve room for checkmarks, labels and hints#

The rows need more structure than right-aligned text. A checked option needs a leading marker slot; a shortcut or submenu cue needs trailing space. In this design, leading is right in Arabic and trailing is left.

Use the same leading reservation on ordinary items so their labels line up with checked items. Do not add padding only after an option becomes checked; that would move its label during the state change.

css
.recording-menu {  box-sizing: border-box;  inline-size: min(19rem,    var(--radix-dropdown-menu-content-available-width, calc(100vw - 1rem)));  max-block-size: min(28rem,    var(--radix-dropdown-menu-content-available-height, calc(100dvh - 1rem)));  overflow: auto;  padding: 0.375rem;  border: 1px solid currentColor;  border-radius: 0.5rem;  background: Canvas;  color: CanvasText;} .recording-menu__item {  position: relative;  display: flex;  align-items: flex-start;  gap: 0.75rem;  padding-block: 0.625rem;  padding-inline: 2rem 0.75rem;  text-align: start;} .recording-menu__label {  flex: 1;  min-inline-size: 0;  overflow-wrap: anywhere;} .recording-menu__indicator {  position: absolute;  inset-inline-start: 0.5rem;  inset-block-start: 0.625rem;} .recording-menu__hint {  flex: none;  margin-inline-start: auto;  font: inherit;  unicode-bidi: isolate;} .recording-menu__item[data-highlighted] {  outline: 2px solid currentColor;  outline-offset: -2px;} .recording-menu__separator {  block-size: 1px;  margin-block: 0.375rem;  background: currentColor;}

The marker aligns with the first line of a wrapping label. If the design instead centers it across the whole row, make that choice explicit and test multi-line labels. A checkmark's shape does not need reflection merely because its column changes sides.

The checkbox component supplies checked-state semantics; a decorative checkmark alone would not. MDN's menuitemcheckbox reference explains that state. For mutually exclusive choices, use the appropriate radio-menu model rather than independent checkboxes that permit an invalid combination.

Keep shortcut text as one LTR unit in its own slot. Do not reverse the chord's characters to match the row. The display hint in this example is decorative; if the application exposes shortcuts to assistive technology, use its supported shortcut metadata or help interface as well. MDN notes that aria-keyshortcuts describes a shortcut but does not implement it.

Let submenus prefer a side without promising that side#

In the recording menu's RTL convention, an export branch prefers opening toward inline end, to the left. The submenu trigger's cue reflects that convention. In an English counterpart, the preferred side and cue point right. Upward or downward ordering within the branch does not reverse with language.

Collision handling can place a branch on the other side when the preferred side lacks space. A submenu near the left viewport edge may therefore open right in Arabic. That can be correct fallback behavior; forcing it left can hide export commands off-screen.

Distinguish a submenu cue from a popup's pointer arrow. The cue communicates the hierarchy's preferred opening direction. A pointer attached to the floating panel must point at its actual trigger after placement resolves. If your design requires the trigger cue to track the resolved side too, connect it to that resolved state deliberately instead of applying another unconditional RTL flip.

Inspect the rendered submenu's placement data when diagnosing a flipped branch. Radix exposes side and alignment data for the content. Review requested preference, resolved side and available space together; the screenshot alone does not identify which one changed.

A portal also changes the pointer relationship between parent and child menus. The submenu is not necessarily a DOM descendant of its visible parent row. A custom mouseleave handler that closes the entire menu immediately can destroy the library's pointer transit behavior. Let the menu component manage that relationship and test diagonal movement into the branch.

Keep keyboard conventions separate from collision geometry#

Vertical menu items retain top-to-bottom order in either locale. ArrowDown moves through later items and ArrowUp through earlier ones. Reversing a flex column because the interface is Arabic would change the visible order without creating an appropriate menu model.

Horizontal keys need the component's direction-aware submenu convention. In this Radix example, ArrowLeft opens the RTL branch and ArrowRight closes it from within. The underlying menu implementation defines separate open and close keys for LTR and RTL.

Those bindings are configured from reading direction. Do not assume they invert again whenever collision handling flips the panel to the other side. Test the library's actual convention at both edges and document the intended behavior for your product instead of wrapping its handlers with a second geometry-based inversion.

Check the opening and dismissal sequence too. Enter or Space on the trigger should open the command menu with its keyboard entry behavior. Escape should dismiss the appropriate menu layer and restore focus according to the component's model. The WAI menu and menubar guidance provides the broader menu interaction model.

Test Tab behavior against your chosen implementation rather than assuming every visually similar dropdown has the same policy. A disclosure of links belongs in the ordinary Tab sequence; a command menu is a composite widget with its own managed focus. Changing roles without changing that policy creates an inaccessible mixture.

If the component supports typeahead, test Arabic labels and an Arabic keyboard layout. Do not infer Arabic matching from an English demonstration. Repeated initial letters, diacritics and alternate spellings can expose assumptions about matching, while a physical-key handler can disagree with the characters the visitor actually enters.

Keep typeahead text separate from decoration#

The explicit textValue props in the example name each command for the library's text matching. They exclude the checkmark, shortcut hint and submenu cue. Use the same translated label source for that value and the visible command, so changing language does not leave an English search string behind an Arabic item.

This distinction matters because accessible names and DOM text are different representations. aria-hidden="true" keeps a decorative hint out of the accessible name, but it does not remove that hint from textContent. A library deriving its matching string from the entire item can therefore include characters that visitors do not consider part of the command name.

Radix's menu source uses item text content as its default matching text when no explicit value is supplied. The provided textValue avoids depending on the details of the row's decorative markup. Adding a leading checked marker or changing a shortcut should not alter how an Arabic visitor finds the notes option.

Keep matching adjustments local to search behavior. Do not remove diacritics or change the stored Arabic labels merely to make a particular typeahead implementation match them. Verify the library's supported matching behavior, and retain Up and Down navigation as a usable route through every available command.

Constrain the whole menu, not just its outer position#

The CSS uses the component's available-width and available-height variables rather than assuming the full viewport is available beneath every trigger. It permits labels to wrap and the menu to scroll when needed. A wide minimum size can defeat a narrower maximum size, so do not add a fixed minimum without testing the constrained state.

Check ordinary content as well as extreme labels. The notes option is longer than either export action and shares a row with a marker reservation. A long shortcut consumes additional trailing space. The label needs a shrinking and wrapping policy; clipping the complete row hides information and can leave several commands indistinguishable.

Positioned content inside a clipped recording card can fail differently from portaled content. The inline CSS variant may be cut off by an ancestor even when the page has no horizontal scrollbar. A portal avoids that particular ancestry but still needs collision handling against the intended boundary.

Do not overwrite the library's generated physical offsets with logical positioning rules. Its resolved coordinates already describe its chosen frame. Set alignment through the API, style content using logical spacing and inspect the final panel bounds. Keep those three responsibilities separate.

For a phone viewport that cannot accommodate a readable cascade, consider a single-panel drill-in presentation. Entering export can replace the root commands with export choices and a named return action. Preserve the submenu's meaning, focus target and selected options rather than shrinking two adjacent panels until neither is readable.

Remember that the menu width is shared space. Borders and container padding consume part of it, then each row reserves its marker column, gaps and any shortcut hint. The label receives what remains. Increasing the panel by a small amount may not resolve a row whose trailing hint has also grown; inspect the individual boxes rather than judging only the outer rectangle.

If the parent menu scrolls while its export branch is open, verify that the branch stays attached to the same visible trigger or closes according to the component's policy. A submenu left floating beside a different row is not fixed by containing it within the viewport. Repeat this with the parent row near the top and bottom scroll boundaries, where a branch may need another placement.

Make submenu access work without hover#

On a touch device, the export row must have an explicit activation route into its branch. A hover-only cascade leaves no equivalent path. Tapping the branch trigger should reveal export choices, while tapping a particular export command performs that choice. Do not let one broad click handler on the row container treat both interactions as the same operation.

Check what happens when a finger scrolls the menu. Beginning a drag over an item should not immediately execute an export on pointer down. Use the component's selection events and supported pointer handling instead of registering another early command handler solely to make touch feel faster.

Outside interaction also belongs to the menu's interaction policy. The example retains the library's default modal setting; changing it affects focus and background interaction, not just placement. Verify whether dismissal and the underlying page's action occur as intended when the visitor taps another recording control. Do not change that setting as an RTL styling workaround.

Keep command identity and focus intact during updates#

The menu operates on a particular recording. Keep that identity attached to its commands while it is open. If a recording list refreshes, a recycled trigger at the same screen position must not silently retarget an open export menu to another recording.

Locale updates need coordinated state too. The example is an Arabic fixture with explicit RTL values. Its English counterpart must update the library direction, rendered content language and direction, translated labels, typeahead values and submenu cue together. Changing only DropdownMenu.Root can repair keyboard behavior while leaving the explicit portal attributes and visible cue describing the previous locale.

In an application, derive those values from the same locale configuration rather than maintaining unrelated flags for the trigger, root menu and export branch. Preserve command identities and the notes setting through that update. If the menu remains open, request any positioning refresh the component requires and verify that focus stays on the same logical item. Remounting the whole recording toolbar simply to change language can remove the trigger or focused item and obscure whether the RTL change itself worked.

When permissions change, update available commands through the component's supported disabled or removal mechanism. A dimmed label with an active selection handler is still an active command. Verify the library's disabled-item focus policy and ensure activation is blocked; different menu implementations need not expose disabled items identically.

Decide whether toggling the notes option closes the menu or leaves it open for more choices. Use the component's selection hooks for that policy. Preventing every selection event indiscriminately can also keep ordinary export commands open after they run, with confusing focus or repeated activation.

If a command opens a dialog, coordinate the menu's closing focus restoration with the dialog's opening focus. The visitor should reach the new dialog's intended target rather than bounce back to the recording trigger after the dialog opens. Use the overlay components' lifecycle hooks and test the complete transition.

Async operations need the same ownership. Report copy or export success from the operation's result, not merely from selecting the menu item. On failure, keep the recording context and a usable retry path. Mirrored labels and menu placement cannot establish that the intended command actually completed.

Keep one dispatch path for each command. The example uses onSelect, which belongs to the menu's activation model. Adding another command call to onClick on the same item can dispatch twice for a pointer action while producing a different result for keyboard activation. Test the host callback count as well as the menu's visual dismissal, and confirm that each export receives the current notes option.

Test each branch in its rendered state#

Create Arabic and English fixtures with the same recording actions and checked option. Translate labels and configure direction together. Compare alignment relationships, hierarchy and command identity rather than expecting every pixel to reflect exactly.

  1. Open the main menu near both outer edges of the toolbar. Confirm the selected start or end alignment and inspect the containing block or portal destination.
  2. Open export by keyboard and pointer. Force a collision on the preferred side, then check fallback placement, diagonal pointer travel and the configured open and close keys.
  3. Toggle notes and reopen. Confirm its visual marker, accessible checked state and the options received by the next export command agree.
  4. Supply a registered shortcut hint and longer approved Arabic labels. Narrow the viewport and enlarge text; verify the marker, label and hint remain distinct and the last command is reachable.
  5. Exercise Escape, outside interaction, disabled commands and trigger removal. Check focus after closing a branch, closing the root menu and opening another overlay.
  6. Test Arabic typeahead if supported, change locale while the menu is open and refresh the recording list. Confirm that commands still target the intended recording.

Repeat with supported browsers and assistive technology. Inspect rendered roles and state as well as geometry: a right-aligned checkbox row can still announce an incorrect checked value, and a correctly named submenu can still be clipped outside its parent card.

A free Ritla scan can report visible text-bearing elements overlapping with R041 and untranslated interactive labels with R011. Verify submenu behavior by interaction; for crowded marker and hint columns, continue with logical inline padding, and use testing RTL across breakpoints for branches that change presentation on phones.

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