RTL direction not working: common causes
Diagnose why a page or component stays LTR after enabling RTL, from missing root markup and local overrides to physical CSS, portals and bidi data.
Guide12 min read
Setting dir="rtl" does not promise that every pixel will move to the other side. It establishes a base direction for text and direction-aware layout. A component can inherit RTL correctly while a right offset, negative transform, SVG arrow or local dir="ltr" keeps part of it in the old orientation.
Start by proving where direction changes. Then debug the layer that remains wrong. Changing the root attribute again will not repair a physical coordinate, and adding row-reverse will not fix a missing document contract.
This guide follows that order: root markup, inheritance, CSS cascade, component boundaries, layout, content and interaction.
Run the two-minute diagnosis#
Open the failing Arabic route and inspect the root element in DevTools.
const root = document.documentElement;
console.table({
lang: root.lang,
dirAttribute: root.getAttribute("dir"),
computedDirection: getComputedStyle(root).direction,
matchesRtl: root.matches(":dir(rtl)"),
});The expected Arabic document state is:
| Check | Expected result |
|---|---|
lang | ar or the supported Arabic locale tag |
dir attribute | rtl |
Computed direction | rtl |
:dir(rtl) | true |
Interpret a mismatch before editing code:
- no
dirattribute and computed LTR: document direction was never set; - no
dirattribute and computed RTL: CSS is supplying direction without HTML markup; dir="rtl"and computed LTR: a CSS rule contradicts the attribute;- root passes but one component fails: inspect the component's nearest direction boundary and physical layout rules;
- component direction passes but a value looks reordered: debug bidi isolation, not the page direction.
This classification turns “RTL is not working” into a testable condition.
Match the symptom to the likely layer#
The visible symptom can narrow the search before you open the cascade.
| Symptom | Inspect first |
|---|---|
| Entire Arabic route remains LTR | Root dir, route locale and initial HTML |
| Text reads RTL but spacing stays on English sides | Physical CSS and generated component styles |
| Page works but a dialog or menu stays LTR | Portal mount, overlay root and library provider |
| One subtree changes back to LTR | Nearest dir boundary and winning direction rule |
| Layout is correct but one code appears reordered | Bidi isolation around that value |
| Visual order looks RTL but Tab moves the other way | DOM order, CSS order and row-reverse |
| Initial paint is LTR, then flips | Server markup and locale hydration |
| Page scrolls sideways after enabling RTL | Physical positioning, transforms and document overflow |
| Icons point the wrong way while text is correct | Asset semantics and directional variants |
| Arabic field starts on the left | Field type, inherited direction and local input rule |
Treat the table as a starting hypothesis. A dialog can have both a portal problem and a physical offset. Prove each layer rather than stopping at the first plausible explanation.
Capture the failing route, state, viewport and content fixture. An empty dialog and the same dialog with a three-line Arabic validation error can take different layout paths.
Cause 1: dir is missing from <html>#
For a predominantly Arabic page, put direction on the root HTML element:
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8">
<title>إدارة الحجوزات</title>
</head>
<body>...</body>
</html>W3C's structural direction guidance recommends this root declaration. Direction then provides the document default and inherits through ordinary descendants unless a closer boundary overrides it.
Do not put dir="rtl" only on the main application container. A modal or notification rendered under body can sit outside that subtree. Root direction gives body-level portals and error surfaces the right default.
R001 reports document direction that is missing, contradicted or declared only in CSS.
Verify the initial server response as well as the hydrated DOM. If JavaScript adds dir after startup, the first render can be LTR and tests that wait for hydration will miss it.
Cause 2: lang="ar" was treated as direction#
Language and direction are independent HTML metadata. lang="ar" identifies Arabic but does not declare an RTL base direction.
Problem
<html lang="ar">Better
<html lang="ar" dir="rtl">The MDN dir documentation explicitly separates language from direction. Without a direction declaration or inherited boundary, HTML uses its default direction rather than deriving one from lang.
Keep separate tests for both attributes. A page can have correct Arabic language metadata and wrong direction, or correct direction with a stale English language tag. R002 reports a missing, non-Arabic or contradictory root language.
Do not derive direction by scanning the current translation text. Map supported locales to their product direction and render both attributes from the route or locale state.
Cause 3: direction exists only in CSS#
This rule can make the page look RTL:
html.locale-ar {
direction: rtl;
}It is still the wrong source of truth for an HTML document. The directional information disappears when the rule is unavailable, and CSS direction does not determine whether :dir(rtl) matches. Selectors Level 4 distinguishes document directionality from stylistic state.
Move direction to markup:
<html class="locale-ar" lang="ar" dir="rtl">Then remove the root direction rule. Keep the locale class only for language or market styling that is not already represented by :lang() or :dir().
The computed style alone cannot diagnose this bug because it reports rtl in both implementations. Assert the actual attribute and selector state too.
The HTML dir="rtl" guide covers the document setup and inheritance pattern in more depth.
Cause 4: a descendant forces LTR#
Direction inherits until a closer boundary changes it. Search the failing component and its ancestors for:
dir="ltr";- inline
style="direction: ltr"; - a class that sets
direction: ltr; unicode-bidiwith an embedding or override;- a third-party root that establishes its own direction;
- a component prop that defaults to LTR.
Inspect the nearest boundary rather than the root again:
// $0 is the element currently selected in DevTools.
const selected = $0;
console.table({
ownDir: selected.getAttribute("dir"),
nearestDirBoundary: selected.closest("[dir]")?.getAttribute("dir"),
computedDirection: getComputedStyle(selected).direction,
});An explicit LTR boundary is valid for an English quotation, email editor or code viewer. It is a defect when it wraps Arabic labels and controls by accident.
R020 reports a forced-LTR container, or a bidi override, over Arabic content. Do not remove a valid LTR boundary globally because one child is Arabic. Narrow the boundary or give the Arabic child its correct local direction.
Cause 5: CSS overrides correct markup#
HTML and CSS can contradict each other:
<main dir="rtl" class="reservation-shell">...</main>.reservation-shell {
direction: ltr;
}The attribute says RTL while computed rendering becomes LTR. Components using :dir(rtl) can select an RTL variant at the same time that layout follows a CSS LTR property.
Use the Styles panel to find the winning declaration, including inline styles and rules with !important. Also inspect unicode-bidi. The MDN unicode-bidi reference warns that overrides bypass parts of the normal bidirectional algorithm and should not be used as a general page-direction fix.
Remember that the CSS all shorthand does not reset direction or unicode-bidi. A component using all: unset can still inherit a directional property you expected to clear. Remove the contradictory declaration or set a legitimate HTML direction boundary.
Do not win the cascade with another !important. That hides the source and leaves future components with two competing direction systems.
Cause 6: the component renders outside the RTL subtree#
Dialogs, menus, tooltips and notifications often render through a portal attached somewhere else in the document. They may look detached from the RTL application because they are detached in the DOM.
If direction is on #app but the dialog is mounted under body, the dialog does not inherit from #app.
Problem
<body>
<div id="app" dir="rtl">...</div>
<div id="dialog-root">...</div>
</body>Better
<html lang="ar" dir="rtl">
<body>
<div id="app">...</div>
<div id="dialog-root">...</div>
</body>
</html>The better structure gives both roots the same inherited document direction. It also preserves the LTR page when the English route renders dir="ltr" on <html>.
If a library mounts content inside its own shadow tree, inspect the host and the library's documented direction API. Shadow content can inherit direction from its host, but a library can establish a local direction or apply physical CSS internally.
Test the rendered overlay, not just the trigger component. Open every direction-sensitive surface in the Arabic route and inspect its actual mount location.
Cause 7: the component library has its own direction contract#
The HTML root remains the source of document direction, but a component library may also need a documented RTL option for presentation it calculates itself. Examples include overlay placement, transition direction, directional icons or generated CSS that contains physical properties.
Do not choose between the two. Keep the library configuration synchronized with the HTML root:
// direction comes from the same locale contract used for document.documentElement.dir.
export function ApplicationProviders({ direction, children }) {
return (
<DirectionProvider direction={direction}>
{children}
</DirectionProvider>
);
}DirectionProvider is the direction API supplied by the chosen component system. Its name and required setup vary, so follow that library's current documentation rather than copying a provider from another stack.
Audit the rendered output:
- Does the provider set semantic
dir, or only select styles? - Are menus and dialogs mounted inside the provider's scope?
- Does the library generate
leftandrightdeclarations at build time? - Are transition signs and arrow assets selected from the same direction state?
- Does a locale switch update existing overlays or only newly mounted ones?
- Are custom wrappers overwriting the library's direction prop?
A provider that flips CSS does not replace dir="rtl" on <html>. Conversely, the root attribute cannot repair a library that calculated a physical placement before the locale changed. Both contracts must agree.
Inspect the browser output instead of assuming the library's “RTL support” covers custom composition. Your spacing overrides, nested grids and icon choices still belong to the application.
Create one integration fixture containing a menu, dialog, tooltip, select and notification. Open every surface under Arabic, change locale while one is open if the product supports that interaction, and assert the actual portal direction and geometry.
Cause 8: the locale changes but the root does not#
Client-side language switching can update strings while leaving the document attributes stale.
const directionByLanguage = {
ar: "rtl",
en: "ltr",
};
export function applyLocale(locale) {
const language = locale.split("-")[0];
document.documentElement.lang = locale;
document.documentElement.dir = directionByLanguage[language] ?? "ltr";
}The map is an application contract. Add every supported RTL language explicitly rather than checking whether the current text contains Arabic letters.
Update attributes in the same locale transaction as translations. Then test deep links, Arabic-to-English switching, English-to-Arabic switching, refresh, sign-in redirects, error boundaries and an overlay that is already open.
If the server renders one direction and hydration immediately chooses another, trace locale initialization. A cookie, URL prefix and cached client state can disagree. Choose one precedence rule and test it.
Cause 9: the layout uses physical CSS#
Correct inherited direction does not mirror left, right, physical margins, transform distances or SVG coordinates. A component can compute to RTL and remain visually pinned to the English side.
Suppose a reservation summary places its status chip at inline end.
Problem
.reservation-summary__status {
position: absolute;
top: 1rem;
right: 1rem;
margin-left: 0.5rem;
}Better
.reservation-summary__status {
position: absolute;
inset-block-start: 1rem;
inset-inline-end: 1rem;
margin-inline-start: 0.5rem;
}Inline end is right in LTR and left in RTL, so the corrected version preserves the English geometry while following direction in Arabic. MDN's logical properties guide explains the mapping.
Audit:
- physical margins, padding and borders;
leftandrightoffsets;- physical border radii;
- floats;
- shadow x-offsets;
- gradients and background positions;
- fixed and sticky positioning.
R030 reports direction-sensitive physical properties without an RTL override. R031 reports physical floats without an RTL override. R032 reports direction-sensitive shadow x-offsets without an RTL counterpart.
Physical values are not always wrong. A real venue map can have a fixed west side. Use logical CSS only where the intent is start or end.
Cause 10: text-align was mistaken for direction#
text-align: right moves line boxes. It does not establish an RTL base direction, change table flow or fix punctuation and mixed runs.
.arabic-copy {
text-align: right;
}If the content is Arabic, inherit dir="rtl" from the document. Use text-align: start when alignment should follow that direction.
The opposite bug also appears: a global style applies text-align: left to Arabic text after the document correctly becomes RTL. R022 reports explicitly left-aligned majority-Arabic text.
Inspect both properties in computed style. If direction is RTL and text-align is left, the base direction works; the alignment rule is overriding presentation. Fix the rule instead of adding another direction attribute.
Cause 11: row-reverse fakes the visual result#
Developers sometimes leave the document LTR and reverse flex rows until navigation appears to begin on the right. That can make one screenshot look correct while DOM, focus and responsive order remain tied to the wrong model.
Use semantic DOM order and real document direction:
.reservation-actions {
display: flex;
gap: 0.75rem;
}With dir="rtl", an ordinary row begins at the RTL inline start. Do not add row-reverse merely because the locale is Arabic.
R081 reports flex row-reverse used to fake RTL instead of document direction, where tab order fights visual order.
Test Tab order and screen-reader order, not only coordinates. If the design calls for a different task order between locales, resolve the product decision in markup rather than stacking CSS order and positive tabindex patches.
Cause 12: transforms and assets stay physical#
Base direction does not flip translateX(), keyframe distances, SVG path data or bitmap images. A side panel, carousel track or directional illustration can move the wrong way even while its labels inherit RTL.
Audit transforms for signed x-axis assumptions:
.reservation-panel {
--exit-step: 100%;
}
.reservation-panel:dir(rtl) {
--exit-step: -100%;
}
.reservation-panel[data-state="closing"] {
transform: translateX(var(--exit-step));
}The custom property makes the direction decision explicit on the moving element. Confirm the correct sign in the actual component and viewport rather than copying the example blindly.
R033 reports a translateX that moves an element off-screen in RTL. R025 reports a directional icon pointing against the sequence its control advances.
Do not mirror every asset. Play, pause, clocks, brand marks and physical maps can keep their orientation. The guide to what should be mirrored in RTL provides the semantic decision model.
Cause 13: the page is RTL but one value is not isolated#
A booking reference can appear reordered next to Arabic even when the document direction is correct. That is a mixed-direction boundary problem, not evidence that dir="rtl" failed.
Problem
<p>مرجع الحجز <span>9276-341-08</span></p>Better
<p>مرجع الحجز <bdi dir="ltr">9276-341-08</bdi></p>The plain span provides no isolation. The <bdi> gives the fixed-order identifier its own LTR boundary while the surrounding paragraph stays RTL.
R021 reports unisolated bidi hazards such as phones, emails and Latin tokens inside Arabic text. The bidirectional text guide explains why source order, copied text and visual order can disagree.
Do not force the entire card to LTR to repair one value. That moves the defect into the Arabic label and punctuation.
Cause 14: form controls have their own value direction#
An Arabic form contains more than Arabic text. Names, addresses and notes may be RTL; email, URL and phone values remain LTR; reservation codes follow their schema.
<label for="guest-note">ملاحظات الضيف</label>
<textarea id="guest-note" name="guestNote"></textarea>
<label for="venue-code">رمز المطعم</label>
<input id="venue-code" name="venueCode" dir="ltr" value="VEN-B6-318">
<label for="contact-url">رابط التواصل</label>
<input id="contact-url" name="contactUrl" type="url" dir="ltr">The Arabic textarea inherits RTL from the page. Machine values get explicit LTR boundaries.
R023 reports text inputs rendered LTR where Arabic input is expected. R070 reports tel, email or url inputs rendered RTL. R072 reports a select rendered LTR with Arabic options.
An empty dir="auto" input is another trap. In Chromium 151, last checked 2026-09-24, it has no strong character to choose RTL and resolves LTR, so a known Arabic placeholder can begin on the wrong side. Use auto only for genuinely unknown multilingual input, not every Arabic field.
Cause 15: overflow makes the page look shifted#
An element extending past the viewport can make an RTL page appear anchored or scrollable on the wrong side. That is geometry after direction, not failed inheritance.
Measure the document once fonts and dynamic content settle:
// page and expect are supplied by the browser test runner.
await page.evaluate(() => document.fonts.ready);
const overflow = await page.evaluate(() => {
const root = document.documentElement;
return Math.max(0, root.scrollWidth - root.clientWidth);
});
expect(overflow).toBeLessThanOrEqual(1);The one-pixel tolerance is a suite choice. Document it, and measure intentional component scrollers separately.
R040 reports document-level horizontal overflow. Do not hide it with overflow-x: hidden on the root before finding the offending box. That can remove access to clipped content.
The RTL overflow guide provides a focused bounding-box debugging method.
Trace the first failing layer in DevTools#
Use a fixed sequence instead of trying random RTL rules.
- Confirm the Arabic URL and locale state.
- Inspect
langanddiron<html>. - Compare the root
dirattribute, computeddirectionand:dir(rtl)match. - Select the failing element and find its nearest
[dir]ancestor. - Search matched CSS for
directionandunicode-bidi. - Check the element's real mount location, including portals and shadow hosts.
- Inspect physical offsets, margins, floats, transforms and asset orientation.
- Decide whether the symptom belongs to layout, DOM order, bidi content or a form value.
- Reproduce the state at the viewport where it fails.
Use a temporary outline to see which box is wrong:
/* DevTools-only diagnostic rule. */
* {
outline: 1px solid rgb(200 0 0 / 0.2);
}Do not commit the universal selector. Narrow the outline to the suspected subtree once you identify it.
If the root contract fails, stop there. Component-level patches made before root direction is correct become compensations that must be removed later.
Add regression tests at the cause#
The test should fail for the defect that shipped, not for a vague screenshot difference.
For document direction:
// page and expect are supplied by the browser test runner.
await page.goto("/ar/reservations");
const root = page.locator("html");
await expect(root).toHaveAttribute("lang", /^ar(?:-|$)/);
await expect(root).toHaveAttribute("dir", "rtl");
await expect(root).toHaveCSS("direction", "rtl");For a portal, open the dialog and assert its computed direction and critical geometry. For a mixed identifier, render the exact fixture and compare its visible state in the supported browser. For a transform, exercise both open and close transitions. For row-reverse, assert focus order.
Keep LTR coverage beside RTL. A logical-property fix must preserve the English layout. A local Arabic override that breaks English is not a complete correction.
Visual baselines help with spacing and assets, but they cannot prove language metadata, focus order or bidi source boundaries. Combine them with DOM and interaction assertions.
Fix RTL at the layer that owns the defect#
When RTL direction appears not to work, the root cause is often more specific: missing root markup, a local LTR boundary, a CSS contradiction, a portal outside the subtree, physical layout, visual reordering or unisolated data. Prove which layer fails before choosing the fix.
A Ritla scan can identify missing or contradicted document direction and expose rendered RTL findings that remain after the root is fixed. Continue with the common RTL bugs guide for cross-component patterns, or the RTL CSS guide when the direction is correct but the layout still follows physical sides.
Checks in this guide
- R001document direction missing, contradicted, or declared only in CSS
- R002html[lang] missing, non-Arabic, or contradicting detected content
- R020Forced-LTR container, or a bidi override, over Arabic content
- R030Direction-sensitive physical properties without an RTL override
- R031float: left/right on content elements in RTL context without override
- R032Direction-sensitive shadow x-offsets without an RTL counterpart
Show every check in this guideShow fewer
- R022text-align: left explicitly set on majority-Arabic text
- R081flex row-reverse faking RTL instead of dir (tab order fights visual order)
- R033translateX moves the element off-screen in RTL
- R025Directional icon pointing against the sequence its control advances
- R021Unisolated bidi hazards: phones, emails, Latin tokens inside Arabic text
- R023Text inputs rendered LTR where Arabic input is expected
- R070tel/email/url inputs rendered RTL (these must stay LTR)
- R072<select> rendered LTR with Arabic options
- R040Document-level horizontal overflow