Why left and right cause RTL bugs

Learn why left and right break RTL layouts across CSS, JavaScript, icons and interface copy, and how to replace each assumption safely.

Guide15 min read

Left and right cause RTL bugs because they are usually asked to mean something else.

margin-left often means “space before this item.” An arrow-right asset often means “continue.” ArrowLeft in a keyboard handler may mean “previous.” The English interface makes those meanings look identical because reading start is left and reading end is right. Arabic separates them. The physical sides stay where they are; the reading sequence begins on the other side.

The browser cannot recover the missing intent. It will keep left on the left, report the left arrow key when the left arrow key is pressed, and move translateX(20px) in the positive x direction. None of those behaviors is confused. The code has used a physical word for a logical job.

Fixing the problem starts with finding every place that the codebase equates a side with a meaning, then deciding whether that use is physical, flow-relative or semantic. The replacement differs for each group. A global swap merely moves the confusion to a new address.

One pair of words is doing several jobs#

The words left and right can describe at least five different things in an interface:

  1. Physical space: the left edge of a viewport, image, diagram or key on a keyboard.
  2. Writing flow: inline start and inline end, which swap physical sides between LTR and RTL.
  3. Sequence: previous and next, whose visual direction depends on the product and content.
  4. Motion: negative and positive movement on a horizontal coordinate axis.
  5. Instruction: user-facing copy such as “choose the right arrow.”

Those meanings agree often enough in English to become one implementation. That shortcut survives until the page direction changes.

Consider a lesson player. In English:

  • the first chapter sits on the left of a row;
  • its start accent is on the left;
  • the completion state is pushed to the right;
  • the next icon points right;
  • the right arrow key may advance;
  • a hover animation moves the icon right;
  • the help text says to use the right arrow.

Only the first three are ordinary layout relationships. The icon, keyboard action, motion and copy are separate decisions. Treating all seven as “swap left and right” is appealing because it is simple. So is throwing the stylesheet down a well.

Direction changes flow, not physical coordinates#

An Arabic page should establish direction in markup:

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

The dir attribute defines the base direction that text and direction-aware layout inherit. lang="ar" identifies Arabic but does not set direction. The HTML direction guide explains where to place dir for whole pages and nested components.

Changing direction affects the inline flow used by text, normal flex rows, grid auto-placement and table rows. In the Chromium 151 behavior measured for this brief, the first item in each of those row layouts sits on the right under RTL.

Direction does not rewrite physical values. These remain physical:

css
.example {
  margin-left: 1rem;
  left: 0;
  text-align: left;
  transform: translateX(1rem);
}

The CSS Logical Properties specification distinguishes physical directions from flow-relative block-start, block-end, inline-start and inline-end. It also gives a useful standard-level judgment: documents need both kinds. A shadow tied to a fixed light source may remain physical, while a paragraph's leading edge should follow writing flow.

This is why dir="rtl" can make a component look half mirrored. The flex row moves, but its margin-left does not. Arabic text starts on the right, but an absolutely positioned action remains at right: 0. The browser has applied direction to the features defined to use it. The other declarations never signed that contract.

A chapter row shows the hidden mismatch#

Take a row in a language-learning product. The chapter title appears at reading start, the completion state appears at reading end, and a colored rule marks the row's start edge.

Problem

html
<a class="chapter-row" href="/lesson/review">
  <span class="chapter-row__title">مراجعة المفردات</span>
  <span class="chapter-row__state">مكتمل</span>
</a>
css
.chapter-row {
  display: flex;
  align-items: center;
  padding-left: 1rem;
  border-left: 0.25rem solid #175cd3;
}

.chapter-row__state {
  margin-left: auto;
}

In an English row, padding-left and border-left decorate inline start. The auto left margin before the state consumes the remaining space and pushes the state to inline end on the right.

Under RTL, the normal flex row already starts on the right. The title appears there and the state follows to its left. The physical border and padding remain on the left, now inline end. margin-left: auto is also on the state's outside edge rather than between the title and state, so it no longer expresses “take the remaining space before this end item.”

Adding row-reverse is not the fix. It reverses a row that direction has already reversed, places the first item on the left and leaves DOM and focus order unchanged. R081 names this use of row-reverse to fake RTL when tab order fights visual order.

Better

css
.chapter-row {
  display: flex;
  align-items: center;
  padding-inline-start: 1rem;
  border-inline-start: 0.25rem solid #175cd3;
}

.chapter-row__state {
  margin-inline-start: auto;
}

In LTR, these logical declarations map to the original left-side declarations, so the English layout is unchanged. In RTL, inline start maps right. The accent and padding move to the right, and the auto start margin on the state consumes the space between it and the title, pushing the state toward inline end on the left.

The useful lesson is not merely “replace left with inline start.” All three physical declarations encoded the same relationship and had to change together. Directional bugs often come in companion declarations:

  • an inset and the padding that reserves room for it;
  • a border and the padding beside it;
  • a side margin and corner radii on the same edge;
  • an icon and the hover transform applied to it;
  • a visual arrow and the accessible label that names its action.

Fix one companion and the component becomes differently wrong. This is progress only in the archival sense.

Ritla check R030 names direction-sensitive physical properties without an RTL override. Review each finding with its nearby declarations and states, not as an isolated line.

Replace physical CSS only when it carries flow meaning#

For a horizontal English layout, common flow-relative replacements are:

Physical declaration in the LTR designLogical declaration when the meaning follows flow
margin-leftmargin-inline-start
margin-rightmargin-inline-end
padding-leftpadding-inline-start
padding-rightpadding-inline-end
border-leftborder-inline-start
border-rightborder-inline-end
leftinset-inline-start
rightinset-inline-end
text-align: lefttext-align: start
text-align: righttext-align: end
float: leftfloat: inline-start
float: rightfloat: inline-end

MDN's guide to logical margins, borders and padding documents the box-model mappings. Its logical positioning guide covers insets, alignment and float values.

The table is a translation of intent, not a replacement script. If right: 1rem means “one rem from reading end,” inset-inline-end preserves the English result and moves correctly in Arabic. If it means “one rem from the physical right edge of a diagram,” keep right.

The same rule applies to top and bottom. In the horizontal writing mode used by English and Arabic, block start is top and block end is bottom. margin-top does not cause an RTL bug merely because it is physical. You may still choose margin-block-start to express flow or support vertical writing, but RTL does not swap the physical top and bottom edges.

Equal values are direction-neutral. padding: 1rem, margin-inline: 1rem, a centered alignment and four equal corner radii do not need side swapping. Do not lengthen symmetric CSS solely to demonstrate commitment.

text-align: left makes Arabic look misplaced#

Text alignment is one of the easiest left assumptions to see. Arabic text normally begins at the right edge of its line. An explicit text-align: left forces it to the opposite physical edge even when the element's direction is RTL.

css
.lesson-summary {
  text-align: start;
}

text-align: start maps according to direction: left in LTR and right in RTL. Use end when the design calls for the trailing text edge. Keep center when the design is centered; it has not taken sides in this dispute.

Ritla check R022 names text-align: left explicitly set on majority-Arabic text. The nearby cause may be a component rule, a utility class, an inherited table style or a state selector. Inspect the winning declaration rather than adding text-align: right !important at the page root. A root override will also right-align LTR values, code samples and nested English content that may have a different contract.

Alignment does not solve bidirectional text inside the line. A phone number, email or mixed identifier can still reorder when its base direction and isolation are wrong. That belongs to bidirectional text handling, not to left-versus-start layout. Moving a corrupted value to the right edge produces a neatly aligned corrupted value.

Absolute positions fail in pairs#

A physically positioned action often has a matching physical padding declaration. Consider a close control placed at the English card's trailing edge:

css
.tip {
  position: relative;
  padding-right: 3rem;
}

.tip__close {
  position: absolute;
  right: 1rem;
  top: 1rem;
}

In Arabic, the text begins on the right, where the close control and reserved space remain. The action overlaps the beginning of the heading or forces a suspicious empty area on the wrong side.

Convert the relationship as a unit:

css
.tip {
  position: relative;
  padding-inline-end: 3rem;
}

.tip__close {
  position: absolute;
  inset-inline-end: 1rem;
  inset-block-start: 1rem;
}

The close control and its reserved space now move together. The containing block still matters: inset-inline-end measures from the positioned ancestor just as right did. Logical positioning fixes the edge, not an unrelated containing-block error.

When a positioned element sits correctly but the page gains horizontal scrolling, inspect its width, transform and containing block. On a right-to-left page, content overhanging the left edge creates document-level horizontal overflow in the Chromium behavior measured for this brief. The RTL overflow guide shows how to find the exact box rather than hiding the evidence with overflow-x: hidden.

Left and right hide inside shorthands and effects#

A source search for left and right will not find every physical assumption.

Four-value shorthands encode physical sides by position:

css
.tip {
  padding: 0.75rem 3rem 1rem 1rem;
  border-radius: 1rem 0 0 1rem;
}

The padding order is top, right, bottom, left. The radius order is top-left, top-right, bottom-right, bottom-left. Neither list reverses under RTL. Expand asymmetric values and name their meaning with logical longhands when they follow flow.

Physical assumptions also hide in:

  • box-shadow horizontal offsets;
  • translateX() and transform matrices;
  • transform-origin;
  • gradient angles and background positions;
  • clip-path and motion paths;
  • SVG path data and viewBox coordinates;
  • pseudo-elements positioned with physical insets;
  • utility names whose generated declaration contains a side.

These do not all have logical equivalents. A horizontal shadow may represent a fixed light source and should stay unchanged. If it is a directional edge treatment, it may need an RTL counterpart; R032 names a direction-sensitive shadow x-offset without one.

translateX() uses a physical x-axis. Its sign does not change with dir. A track or panel that moves off-screen in RTL is the case named by R033. Smaller movements, such as an icon nudge in the wrong direction, still need a manual direction rule even when they remain visible.

Do not mirror the entire component with scaleX(-1). That flips text, images and every physical coordinate inside it. Mirror only the asset or effect whose meaning changes. The blunt fix is admirably thorough and wrong in several places at once.

Icon names can confuse shape with action#

An asset named arrow-right has a clear physical shape. Trouble begins when code treats that shape as a permanent synonym for “next.”

In a sequential lesson interface, previous and next are semantic actions. Their physical arrows may change with interface direction. In a map, a right-pointing arrow may literally mean east or turn right and should remain physical. The same SVG shape can be correct in one control and wrong in another.

Name component responsibilities semantically:

js
const lessonActions = {
  previous: "previous",
  next: "next",
};

Then let the icon component choose or mirror the appropriate directional shape from the element's direction. Keep the underlying asset name physical if it describes the drawing. arrow-right.svg is honest about what it contains; next.svg is a promise about behavior and therefore needs direction-aware rendering.

Test the whole control:

  • Does the next icon point along the sequence used by the interface?
  • Does activating it advance to the expected item?
  • Does its accessible name say “next lesson” rather than “right arrow”?
  • Does the focus sequence still follow the chapter order?
  • Does a hover or press animation move with the same meaning?

Ritla check R025 names a directional icon pointing against the sequence its control advances. The icon can be visually mirrored and still wrong if the click handler advances in the opposite sequence, so test behavior rather than admiring the SVG.

Arrow keys are physical inputs, not semantic actions#

The browser reports the physical arrow key through values such as ArrowLeft and ArrowRight. The KeyboardEvent.key documentation lists these predefined values. It does not translate them into previous, next, inline start or inline end for your component.

Decide what the component promises. A spatial editor may need the left key to move a selected object physically left in both languages. A chapter strip may define arrow keys relative to reading sequence. Those are different controls, so they should not share a helper called goRight() and hope context will become apparent later.

For a strip where index +1 means move toward inline end, the mapping can be explicit:

js
function inlineStepForKey(key, direction) {
  if (key === "ArrowRight") return direction === "rtl" ? -1 : 1;
  if (key === "ArrowLeft") return direction === "rtl" ? 1 : -1;
  return 0;
}

Define direction from the component's computed or declared direction, not from the user's locale alone. An LTR widget can live inside an Arabic page. Test the handler with the actual focus order and selection model; the function defines one sequence policy, not a universal keyboard law.

Avoid deprecated numeric key codes. Read event.key, document whether the behavior is spatial or sequential, and prevent the default action only when the component has actually handled the key. A keyboard is not an RTL layout. Its left key remains over there, on the left.

scrollLeft is physical by name and directional in behavior#

Horizontal scrolling adds a particularly cheerful complication. The API is named scrollLeft, but an RTL scroll container starts with scrollLeft at 0 on its rightmost, content-start position and becomes increasingly negative toward content end in current browser behavior documented by MDN.

That means code written around positive LTR offsets may do nothing or move toward the wrong end under RTL. The brief's Chromium 151 measurement found the same pattern: a positive left offset at RTL start does nothing, while negative values move toward end.

Do not scatter direction checks around every button. Define the operation the product needs:

  • scroll toward inline start;
  • scroll toward inline end;
  • reveal the previous chapter;
  • reveal the next chapter;
  • scroll to a particular item.

Where possible, scroll the target item into view instead of calculating a physical left offset. If the component must reason about scrollLeft, wrap the API behind a helper that documents its origin and sign, and test start, middle and end in both directions. Safari can report values beyond the nominal range during overscroll, so clamp calculations if those values feed progress or button states.

A variable called distanceFromStart is easier to review than one called left. The first states a contract. The second states where the browser API began its historical journey.

Interface copy can preserve the wrong direction#

CSS may be fixed while the instructions still say “select the right arrow” or “open the panel on the left.” If the control or panel mirrors, the copy is now false.

Prefer semantic instructions when the physical side is not the point:

  • “Continue to the next lesson.”
  • “Open the chapter panel.”
  • “Use the previous and next controls.”
  • “Move to the beginning of the sequence.”

Keep physical wording when users genuinely need a physical direction, such as moving an object left on a canvas or locating a fixed map region. Translate the instruction, but do not change its geometry to sound more international.

Review visible text, tooltips, onboarding steps, empty-state help, aria-label values and test assertions. A control can show the correct icon while its accessible name remains “right arrow.” R082 specifically names UI text that references physical arrow directions. It does not decide whether a physical instruction is valid; product context does.

Avoid storing left and right inside translation keys when the meaning is semantic:

text
lesson.next
lesson.previous
panel.open
panel.close

Keys such as lesson.rightArrow bind content to one rendering. Translation systems are already asked to manage language. They need not preserve an obsolete screenshot as well.

Nested direction makes global swaps unreliable#

An Arabic page can contain an English code editor, chart, embedded report or other LTR component. A rule such as this matches every descendant of the RTL boundary:

css
[dir="rtl"] .chapter-row {
  margin-right: 1rem;
  margin-left: 0;
}

It also matches .chapter-row inside a nested dir="ltr" subtree because the RTL ancestor still exists. The selector describes ancestry, not the element's current direction.

Logical properties are usually better because they map from the styled element's direction. For direction-specific effects that have no logical form, use :dir(rtl) when the rule should follow the element's actual directionality:

css
.chapter-link:dir(rtl) .chapter-link__icon {
  transform: scaleX(-1);
}

This relies on direction established through HTML. In Chromium 151, setting only direction: rtl in CSS does not make :dir(rtl) match. Another reason to set the dir attribute at the correct boundary rather than asking CSS to invent document semantics.

The edge case is deliberate root theming. If a rule truly belongs to an explicit Arabic document variant rather than the descendant's own direction, an attribute selector may be exactly right. Choose the selector based on whose direction matters.

Physical and logical properties can collide#

During migration, teams often add a logical declaration without removing the old physical one:

css
.chapter-row {
  margin-inline-start: 1rem;
  margin-left: 0;
}

In LTR, margin-inline-start maps to the left margin. The later margin-left wins, so the space disappears. In RTL, inline start maps right and the left reset affects the other side, so the space remains.

The CSS Logical Properties specification defines physical and flow-relative properties in logical property groups. Once the browser maps them for the element's writing mode, paired declarations share a computed value and participate in the cascade together.

Inspect source order, specificity, cascade layers and broad shorthands. A later margin: 0, padding: 0 or border: 0 can reset a side previously set by a logical declaration. State and responsive selectors can reintroduce the old physical property after the base component was converted.

If a fix works only in LTR or only in RTL, open DevTools and inspect:

  1. the element's computed direction;
  2. the physical side used by each logical property;
  3. crossed-out declarations on that side;
  4. later shorthands and !important rules;
  5. nested direction attributes.

Do not add another override until you know which declaration won. CSS already has a cascade. Adding a private second cascade through guesswork is unnecessary administrative growth.

Search beyond the stylesheet#

A useful audit searches source, generated output and product content. Look for:

text
left
right
*-left
*-right
translateX
box-shadow
background-position
scrollLeft
offsetLeft
clientX
pageX
ArrowLeft
ArrowRight
arrow-left
arrow-right
chevron-left
chevron-right
swipe left
swipe right

Then classify each result:

MeaningExampleAction
Reading start or endSpace before a chapter titleUse logical CSS
Previous or nextLesson navigationUse semantic action and direction-aware rendering
Physical coordinateCanvas movement or map markerKeep left or right
Directional effectIcon nudge or entering panelAdd a scoped RTL value
Physical inputArrow key identity or pointer x coordinateKeep input, map to component meaning
User instruction“Select the right arrow”Rewrite semantically unless the side is genuinely physical

Include component props, design tokens, analytics names, snapshot text and test locators. An internal event called rightArrowClicked may continue collecting data after the control becomes “next,” leaving dashboards and tests tied to the former rendering.

Do not rename every historical identifier during the first visual fix if that would obscure behavior changes. Record the semantic debt and migrate it deliberately. The priority is to stop one physical word from controlling layout, action and reporting without anyone noticing.

Test the relationship in both directions#

Use the same component markup and data under dir="ltr" and dir="rtl". Verify each layer separately:

LayerWhat to test
LayoutStart spacing, end actions, borders and radii move together
OrderDOM, reading and focus sequence remain meaningful
IconDirection matches the action sequence
MotionAnimation moves toward its intended logical or physical destination
KeyboardBoth arrow keys perform the documented spatial or sequential action
ScrollStart, middle and end states work in both directions
CopyVisible and accessible instructions remain true
ExceptionsMaps, charts and fixed coordinates stay physically fixed

Test long Arabic labels, narrow viewports and every state that changes directional styles. A selected row may reintroduce border-left; a phone media query may restore right; a disabled icon may use a different asset. Open menus and panels during the test. Closed components are famously well behaved.

Tab through the interface and confirm that focus follows the meaningful sequence rather than a visual order manufactured by row-reverse or order. Activate previous and next controls, not just their screenshots. At the scroll boundaries, verify buttons disable according to content start and end rather than a positive or negative scrollLeft assumption.

Keep physical exceptions in the matrix. The goal is not for every left-side value to move. The goal is for each relationship to behave according to its meaning.

Replace the assumption, not merely the word#

Left and right become RTL bugs when they stand in for start, end, previous or next. Fix the abstraction at the layer where it appears: logical properties for layout, semantic actions for navigation, scoped direction rules for physical effects, documented mappings for input, and wording that describes the task rather than one screenshot.

A free Ritla scan can surface direction-sensitive physical properties without an RTL override R030, left or right floats without one R031, directional shadow offsets without a counterpart R032 and directional icons that point against their control's sequence R025. Use the common RTL bugs guide when the same physical assumption appears alongside bidi, typography or localization failures.

Keep left and right where they truly mean left and right. Everywhere else, give the relationship its real name. The code becomes clearer before it even becomes RTL-safe, which is almost suspiciously convenient.

Checks in this guide

Show every check in this guideShow fewer

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.