Skip to content

Debugging RTL layout problems

A practical workflow for tracing RTL layout bugs through document direction, computed styles, positioned elements, and overflow.

By , Founder of Ritla

Guide13 min read

The Arabic page looks fine until someone opens a menu. Then the menu appears partly off-screen and the whole page starts scrolling sideways. You can spend an afternoon adding overflow-x: hidden, or you can find out which box moved and which rule moved it. The second option is less dramatic, but it tends to survive the next release.

Debugging RTL layout problems is mostly a matter of separating three questions: What direction does the browser think this element has? Where is its box? Which rule put it there? Answer them in that order. A screenshot tells you that something is wrong; the browser's computed styles and geometry tell you what to change.

The examples below concern a bookshop's stock-transfer dashboard. Its English and Arabic pages share a header with an options button and a small menu. The menu works in English and hangs off the left edge in Arabic. That is one failure, not a claim that all Arabic layout bugs are menus. It gives us something specific to investigate.

Reproduce one failing state#

First, write down the state in which the bug exists. “The Arabic layout breaks on mobile” is not enough to reproduce a bug, let alone know whether you fixed it. Record the route, locale, viewport width, zoom level, browser, whether the menu is open, and the exact content that makes it fail. Keep the English version of the same component nearby for comparison.

For the stock-transfer header, a useful report might say: the Arabic route at a narrow viewport, with the options menu open and the longer branch name in the title, can scroll horizontally; the English route at the same width cannot. The comparison matters because a rule may be wrong in both directions while only the Arabic page has enough text to expose it. It also stops you from changing the English placement while fixing Arabic.

Start from a clean page load and repeat the action that reveals the problem. An open popover, a validation message, a loaded font, or a translated button label can change the geometry after the initial screenshot. Inspect the element in that state. Do not debug a closed menu and assume you have learned anything about its open position. An impressive amount of CSS debugging consists of looking at the wrong state with great concentration.

If the issue appears only at one breakpoint, test just above and below it. If it appears only after data loads, preserve that data. If the Arabic route uses a different component tree, note that before calling it an RTL bug. Direction may be the trigger, but it is not automatically the cause.

Confirm direction before touching layout#

Look at the root element in the Elements panel. An Arabic page should normally declare both its language and its base direction:

html
<html lang="ar" dir="rtl">

lang="ar" does not make a page RTL. The HTML dir attribute sets the base direction; without an explicit direction, the document generally defaults to LTR. This was also measured in Chromium 151 for the behaviors discussed here. Set direction on the document, then use local dir attributes only where content genuinely needs a different base direction. The HTML direction guide covers that decision in more detail. R001 concerns document direction that is missing, contradicted, or declared only in CSS.

Now select the troublesome element. In Chromium DevTools, $0 refers to the selected element. Run:

js
getComputedStyle($0).direction
$0.getAttribute("dir")
$0.closest("[dir]")

These answer related but different questions. The first gives the resolved CSS direction used by the element. The second reports a direction attribute on that element, if there is one. The third finds the nearest ancestor with a dir attribute; it does not prove that no intervening CSS rule overrides the inherited direction. Check the matched rules and each ancestor when those answers disagree.

A component can display RTL because a stylesheet says direction: rtl while its HTML still has no dir. In the Chromium behavior checked for this guide, :dir(rtl) then does not match that component: the selector follows HTML directionality, not a CSS-only change to direction. That discrepancy is a useful clue when an RTL-specific rule appears to be missing. The fix for a known Arabic page is usually the appropriate dir attribute, not another selector to compensate for the first omission. See the difference between dir and CSS direction if the two are being mixed in your codebase.

Do not assume every child should compute to RTL. A deliberately LTR code value or other known-direction content can have a local direction. But a wrapper around Arabic navigation that forces direction: ltr, or uses a bidi override, deserves scrutiny. R020 is specifically about a forced-LTR container or bidi override over Arabic content. It is not a general warning about normal direction inheritance.

Find the first wrong box#

Once direction is established, stop judging the page as a whole. Locate the first element whose rectangle differs from the intended layout. Hover the node in DevTools to see its highlighted box. Is the menu itself off-screen, or is its positioned parent off-screen? Does the title take all the width before the menu opens? Is the visible text outside its box, or is the entire box outside the viewport? Those are different repairs.

For a selected element, the bounding rectangle gives coordinates relative to the viewport:

js
const rect = $0.getBoundingClientRect();
({ left: rect.left, right: rect.right, width: rect.width, viewport: innerWidth });

Select the menu, then its parent, then the header. Compare those rectangles at the same scroll position. A menu with left below zero is a plausible cause of the visible overhang. A parent whose rectangle is inside the viewport while its child is outside points you toward the child's positioning or size. A title already wider than the header points you toward text sizing or flex constraints instead. The rectangle includes padding and border, and transforms affect the geometry you observe. It is a measurement, not a diagnosis by itself.

For a page that scrolls sideways, the document's width is another quick signal:

js
({
  contentWidth: document.documentElement.scrollWidth,
  viewportWidth: document.documentElement.clientWidth
});

If contentWidth is larger, inspect the elements that extend toward the scrollable edge. In the checked Chromium behavior, overhang past the left edge of an RTL document produces horizontal scrolling; an equivalent overhang past the right edge does not. LTR is the reverse. A scrollbar is therefore evidence about which side matters, not proof that the first wide-looking node you find is guilty. For a longer investigation, the horizontal-overflow guide covers the search and containment cases in depth. R040 concerns document-level horizontal overflow.

Remember that a child inside an intentionally scrollable carousel may lie beyond the viewport without enlarging the document. A fixed overlay can also be visually outside the viewport without changing normal document flow. Look at the actual scroll container and the visible failure before deleting a rule.

Trace the rule that placed it there#

In the Styles panel, find the matched declaration for the misplaced element. In the Computed panel, confirm the value that won. Then toggle one relevant declaration at a time. If removing right: 0 moves the menu back into view, you have a lead. If removing width fixes it but removing right does not, the problem may be available space rather than anchoring.

Search the matched rules for physical sides: left, right, margin-left, padding-right, physical border radii, and horizontal transforms. These values do not become their RTL counterparts when dir changes. A rule such as right: 0 still attaches to the physical right edge. Logical properties instead name an edge in the writing direction; MDN's logical positioning reference explains the mapping. R030 concerns direction-sensitive physical properties without an RTL override. It does not mean every use of left or right is a bug. A fixed viewport control intentionally pinned to the physical right can be exactly right.

Check the element's positioning context as well. An absolutely positioned child is placed against its containing block, commonly the nearest positioned ancestor, and is removed from normal flow. The CSS positioning reference is useful here. If the intended parent lacks position: relative, the menu may be anchored to some other box. You cannot repair that by swapping right for left; first make sure it is positioned against the right element.

Media queries are another hiding place. The desktop rule may use a logical inset, while a narrow breakpoint restores right: 0 or a fixed width. DevTools shows which declaration wins at the failing width. Read that list before editing the rule you happen to remember writing.

Be careful with the word computed here. The Computed panel tells you the winning value for a property, but it does not tell you why that value won or whether it expresses the design's intent. If you see right: 0, trace it back to the source rule and its selector. If you see both a physical inset and a logical inset in matched rules, check which one wins at that width and in that direction. A later declaration can undo a perfectly sensible earlier fix. Temporarily disable one declaration, observe the rectangle, then restore it before trying the next. Changing five rules at once makes a clean screenshot and a poor explanation.

The visible thing may not be the selected element at all. An arrow or accent drawn with ::before or ::after has its own positioning rules. An SVG icon can have a fixed drawing direction while the surrounding button moves correctly. Inspect the pseudo-element or SVG before adding a transform to the button. Its box may be exactly where it belongs; its artwork may point the wrong way. That is a different test from the menu's off-screen rectangle, and the mirroring guide explains which visuals should actually change direction.

Finally, distinguish geometry from painting. A transform can move what you see without changing normal flow. z-index can determine which box covers another, but cannot move a box back inside the viewport. If the rectangle is correct and the menu is merely behind the header, inspect stacking. If the rectangle is off-screen, raising its stacking order is decoration on a problem.

A menu that hangs off the Arabic page#

Here is a static, open-state fixture for the bookshop header. The trigger is at the end of the flex row, so it sits at the right in LTR and at the left in RTL. The menu is positioned against the trigger wrapper. The HTML is deliberately plain so we can inspect placement; a production menu also needs its own interaction and accessibility behavior.

html
<section class="transfer" lang="ar" dir="rtl">
  <header class="transfer__header">
    <h2>طلبات نقل الكتب إلى فرع الجامعة</h2>
    <div class="transfer__actions">
      <button type="button">خيارات النقل</button>
      <div class="transfer__menu">
        <button type="button">تعديل موعد النقل</button>
      </div>
    </div>
  </header>
</section>

The shared layout rules are:

css
.transfer__header {
  display: flex;
  align-items: start;
  justify-content: space-between;
  gap: 1rem;
}

.transfer__header h2 {
  min-inline-size: 0;
}

.transfer__actions {
  position: relative;
  flex: none;
}

Now the problematic rule:

css
.transfer__menu {
  position: absolute;
  top: 100%;
  right: 0;
  inline-size: 16rem;
}

In LTR, the actions wrapper is at the right end of the header. Anchoring the menu's right edge to that wrapper's right edge makes it extend leftward into the page. In RTL, the wrapper is at the left end. The same physical right: 0 still makes the menu extend leftward, this time beyond the page edge when there is not enough room. In Chromium 151, that left-side overhang can give the RTL document horizontal scroll. The browser has followed every instruction it was given. The instructions just disagree with each other.

Do not patch this with overflow-x: hidden on the page. That can conceal the scrollbar while leaving the menu unreachable. Nor should you reverse the whole header row: its first DOM child already appears at the right under RTL, and row-reverse would undo that direction change.

The menu is meant to attach to the inline end of its trigger. Change the physical inset to a logical one:

css
.transfer__menu {
  position: absolute;
  inset-block-start: 100%;
  inset-inline-end: 0;
  inline-size: 16rem;
}

inset-inline-end maps to right in a horizontal LTR context and left in a horizontal RTL context. The English placement stays as it was; the Arabic menu extends toward the interior of the page. inset-block-start replaces the vertical top here for consistency, although top was not the source of this bug.

That is a directional fix, not a promise that a 16rem menu fits every phone. If the viewport or the trigger's container is narrower than the menu, it may still exceed the available space. Test the narrowest supported width, and decide whether the design should constrain the menu's inline size, wrap its content, or use a different small-screen presentation. Direction-aware CSS cannot manufacture space.

Tell placement from sizing#

Several failures can produce the same screenshot. A misplaced box has room but is attached to the wrong edge. An oversized box is attached correctly but cannot fit. A clipped box fits, yet its content is hidden. Treating all three with overflow: hidden replaces one bug with three quieter ones.

For sizing, compare the element's rectangle with its parent's rectangle. Inspect width, min-width, max-width, white-space, overflow, and flex or grid tracks in the Computed panel. Arabic text can be longer than the English label in the space allotted by a fixed-pixel design. A card with width: 180px may be the limiting box, not the direction rule. R043 concerns a fixed-pixel-width container whose Arabic text exceeds its space. R041 concerns visible text-bearing elements overlapping; R042 concerns clipped text where content is wider than a box with overflow: hidden and no ellipsis. These are different symptoms, so inspect which one you have before choosing a fix.

In a flex row, a child may resist shrinking to the width you expected because of its intrinsic minimum. min-inline-size: 0 on the text-bearing flex item can let it shrink, but only if the product design allows that text to wrap or truncate. Inspect the actual line breaks and decide whether hiding the branch name is acceptable. For our header, the title should wrap while the options button stays usable; that is why the fixture lets the heading shrink and keeps the actions wrapper from flexing. The flex layout reference explains how flex sizing and minimum content size interact.

Also check the font that actually rendered. If an Arabic font fails to load and the browser uses a fallback with different glyph widths, the header can change after first paint. R050 concerns Arabic text falling back past the declared font family, including a webfont that failed to load. Fix the font loading problem before measuring the final line breaks. A width chosen around the fallback font may fail again when the intended font arrives.

If the box fits and only a Latin token, punctuation mark, or number looks displaced inside Arabic text, you have moved from layout into bidirectional text. The Unicode bidi algorithm orders text runs inside the box; CSS positioning moves the box itself. Do not mirror an entire component to repair one value. The bidi guide covers isolation and mixed-direction content. R021 is for unisolated phone numbers, emails, and Latin tokens inside Arabic text, not a generic label for every visually surprising word.

Check source order and interaction#

The layout can appear fixed while the component remains awkward to use. After the geometry looks right, Tab through the controls. Open the menu using the keyboard. Confirm that focus reaches its items in a sensible order and that the trigger is still visible at the narrow width. CSS cannot make an off-screen menu usable just because the page no longer scrolls.

Pay particular attention to flex-direction: row-reverse. On an RTL flex row, the initial order already runs from the right. In the checked Chromium behavior, adding row-reverse lays the items out left-to-right in DOM order, and Tab moves left-to-right. It may be an intentional design, but it is a poor substitute for setting the page's direction. R081 concerns row-reverse used to fake RTL when tab order fights the visual order. Inspect the DOM and keyboard path together rather than declaring the screenshot correct on sight.

If the menu appears behind another element, inspect its stacking context separately from its edge position. If it opens above the trigger, check the vertical anchor and available space. If it is clipped by a parent with overflow: hidden, the parent may need a different overflow strategy or the menu may need to render outside that clipping ancestor. None of those is solved by changing document direction.

Verify the fix instead of trusting the screenshot#

Test the same component in LTR and RTL with the same state: menu open, longest realistic title, and the same viewport sizes. Check a width just above the layout breakpoint, one just below it, and the narrowest supported width. Measure the menu and header rectangles again. On each route, the menu should remain within the intended visible area and attach to the intended edge of the button; the page should not gain document-level horizontal scroll when it opens.

Then repeat after the page's webfonts load, with a translated error or status message if that state affects the header, and at increased text size or zoom. The exact breakpoint may no longer be the exact failing width. Test keyboard order and the clickable area, not only pixels. A menu that fits but covers the button that closes it is still a bug, merely one with better manners.

For a regression test, record behavior, not the one pixel coordinate you happened to see. A browser test can open the transfer menu in each direction, measure its bounding rectangle, and assert that the menu stays within the viewport at the chosen widths. Give that test a long Arabic title so the shrinking behavior is exercised. Pair it with a visual comparison when the relative alignment matters. The test should also confirm that LTR placement did not change, since the point of replacing right with inset-inline-end was to preserve it.

Keep the bug report's cause and correction together: “right: 0 anchored the menu to the physical right of a trigger at the RTL header's left edge; inset-inline-end: 0 anchors it to the direction-aware end instead.” That sentence helps the next engineer review a similar component. “Fixed RTL” does not.

A free Ritla scan can surface document-level horizontal overflow R040 and direction-sensitive physical properties without an RTL override R030, giving you concrete places to inspect. It cannot tell you whether this particular menu is pleasant to use with a keyboard or whether its open state fits every viewport. For those last checks, keep your browser open and the menu open too. If overflow persists after the positioning fix, continue with the dedicated overflow workflow.

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