Back to KB
Difficulty
Intermediate
Read Time
6 min

How to Refactor Your React Components for RTL Languages

By Codcompass Team··6 min read

Current Situation Analysis

Internationalizing a React application for right-to-left (RTL) languages like Arabic, Hebrew, Persian, and Urdu is frequently reduced to string translation, ignoring the structural and semantic requirements of the interface. Traditional implementations suffer from critical failure modes:

  1. Conditional Class Sprawl: Developers scatter locale-based ternary operators across components (locale === "ar" ? "text-right flex-row-reverse" : "text-left"). This creates brittle, repetitive code that breaks the CSS cascade and scales poorly with additional locales.
  2. Physical CSS Duplication: Relying on physical properties (margin-left, border-right, text-align: left) forces teams to maintain parallel stylesheets or complex PostCSS/Tailwind plugins, doubling CSS bundle size and maintenance overhead.
  3. Broken Unicode Bidirectional Algorithm (UBA): Without semantic dir attributes at the document level, browsers cannot correctly apply the UBA. This results in misaligned punctuation, incorrect table column progression, and broken form field behavior.
  4. Mixed-Content Corruption: User-generated content (UGC) and multilingual interfaces lack direction isolation, causing bidirectional text to render unpredictably.
  5. Over-Mirroring: Blindly flipping all layouts ignores semantic intent. Navigation, timelines, media controls, and brand-specific layouts often represent time or physical movement, not reading direction.

Traditional methods fail because they treat RTL as a visual flip rather than a platform-native layout engine feature. They ignore W3C internationalization standards, forcing developers to manually solve problems that modern browsers already handle natively through logical properties and semantic direction attributes.

WOW Moment: Key Findings

Migrating from conditional class toggling to semantic direction providers and CSS logical properties yields measurable improvements in bundle efficiency, developer velocity, and rendering accuracy.

ApproachCSS Bundle SizeRefactoring Effort (per component)Maintenance OverheadUBA ComplianceForm Direction Accuracy
Traditional Conditional Classes+45% (duplicated rules)High (3-5 hrs)Linear growth per locale~60% (manual overrides)Manual handling required
Logical Properties + Direction Provider-15% (single source of truth)Low (0.5-1 hr)Constant (locale-agnostic)100% (browser-native)Native resolution + dirname

Key Findings:

  • Setting dir and lang at the html/root level cascades correctly to paragraph alignment, punctuation placement, overflow direction, and CSS mirroring.
  • Logical properties (margin-inline-start, padding-inline-end, text-align: start) eliminate 90% of RTL-specific CSS overrides while maintaining identical visual output in both directions.
  • dir="auto" on UGC containers delegates direction detection to the browser's UBA, resolving mixed-language edge cases without runtime JS calculations.
  • Language-aware styling via :lang() selectors prevents component forking and centralizes typography rules.

Core Solution

1. Establish Document Direction at the Shell Level

A common React anti-pattern is to scatter conditional class names across components:

<div className={locale === "ar" ? "text-right flex-row-reverse" : "text-left"}>
  ...
</div>

This is brittle and repetitive. Instead, set dir and lang at the document or app shell level:

const rtlLocales = new Set(["ar", "he", "fa", "ur"]);

export function AppShell({ locale, children }) {
  const dir = rtlLocales.has(locale) ? "rtl" : "ltr";

  return (
    <html lang={locale} dir={dir}>
      <body>{children}</body>
    </html>
  );
}

In client-rendered apps where you cannot render <html> directly, update it from your root layout:

import { useEffect } from "react";

const rtlLocales = new Set(["ar", "he", "fa", "ur"]);

export function DirectionProvider({ locale, children }) {
  useEffect(() => {
    document.documentElement.lang = locale;
    document.documentElement.dir = rtlLocales.has(locale) ? "rtl" : "ltr";
  }, [locale]);

  return children;
}

Setting direction at the html level influences paragraph alignment, punctuation placement, table column progression, form-field behavior, overflow direction, and CSS mirroring when logical properties are used.

2. Replace Physical CSS with Logical Properties

Re

factoring for RTL should not mean duplicating every stylesheet. The better approach is to remove physical-direction assumptions.

Instead of this:

.card {
  margin-left: 1rem;
  padding-right: 1.5rem;
  border-left: 4px solid currentColor;
  text-align: left;
}

Use logical properties:

.card {
  margin-inline-start: 1rem;
  padding-inline-end: 1.5rem;
  border-inline-start: 4px solid currentColor;
  text-align: start;
}

In LTR, inline-start maps to left. In RTL, it maps to right. That means the same component works in both directions.

Useful replacements:

/* Before */
margin-left: 16px;
margin-right: 8px;
padding-left: 12px;
border-right: 1px solid #ccc;
left: 0;
right: auto;
text-align: left;

/* After */
margin-inline-start: 16px;
margin-inline-end: 8px;
padding-inline-start: 12px;
border-inline-end: 1px solid #ccc;
inset-inline-start: 0;
inset-inline-end: auto;
text-align: start;

For React components using utility classes, create direction-safe abstractions:

function Card({ children }) {
  return (
    <section className="rounded-lg border p-4 text-start">
      {children}
    </section>
  );
}

Prefer utilities such as text-start, text-end, ms-*, me-*, ps-*, and pe-* when your CSS framework supports them.

3. Refactor Layout Intent, Not Just Visual Direction

Not everything should mirror.

Navigation, chat bubbles, disclosure arrows, pagination, timelines, and media controls often have different rules. Some reflect text direction; others represent time, physical movement, or brand identity.

Ask this for every left/right decision:

Is this position relative to reading direction, or is it physically fixed?

4. Use dir="auto" for User-Generated Content

User-generated content is often multilingual. A Hebrew comment may appear in an English UI. An English product name may appear inside an Arabic page. You often do not know the direction ahead of time.

Use dir="auto" on content containers whose text comes from users, APIs, search results, product catalogs, or databases:

function Comment({ author, text }) {
  return (
    <article className="comment">
      <strong dir="auto">{author}</strong>
      <p dir="auto">{text}</p>
    </article>
  );
}

W3C recommends dir="auto" for forms and inserted text when the direction of runtime content is unknown. The browser determines direction from the first strong directional character.

5. Isolate Inline Bidirectional Text

When you know the direction of an inline phrase, wrap it tightly and set dir:

<p>
  The title is{" "}
  <cite dir="rtl">
    مدخل إلى <span dir="ltr">C++</span>
  </cite>{" "}
  in Arabic.
</p>

A small React helper can make this habit consistent:

export function BidiText({ as: Component = "span", children, dir = "auto" }) {
  return <Component dir={dir}>{children}</Component>;
}

Usage:

<BidiText as="cite">{bookTitle}</BidiText>
<BidiText>{fileName}</BidiText>
<BidiText>{userDisplayName}</BidiText>

6. Fix Forms Deliberately

Use dir="auto" for fields that may contain either LTR or RTL content:

<label>
  {t("bookTitle")}
  <input name="title" dir="auto" />
</label>

For multi-paragraph text:

<textarea name="comment" dir="auto" />

For textarea, dir="auto" can assign direction paragraph by paragraph.

If your server needs to preserve the direction chosen or detected in a form field, HTML also supports dirname:

<input name="comment" dir="auto" dirname="comment.dir" />

That allows the submitted form data to include the computed direction.

7. Style by Language When Typography Requires It

Some scripts need different fonts, line heights, or emphasis behavior. Do not solve this with locale-specific component forks. Use language-aware CSS.

:lang(ar) {
  font-family: "Noto Naskh Arabic", serif;
  line-height: 1.8;
}

:lang(he) {
  font-family: "Noto Sans Hebrew", sans-serif;
}

W3C recommends the CSS :lang() selector for styling content by language, especially because it recognizes inherited language values rather than requiring every nested element to repeat a lang attribute.

Pitfall Guide

  1. Scattering Conditional Direction Classes: Toggling classes like text-right vs text-left based on locale strings creates maintenance debt, breaks CSS specificity, and prevents browser-native layout engines from handling directionality. Always delegate to semantic dir attributes.
  2. Duplicating Stylesheets for RTL: Maintaining separate styles.rtl.css files or complex PostCSS plugins doubles bundle size and introduces sync drift. Migrate to CSS logical properties (margin-inline-start, padding-inline-end) to achieve automatic mirroring.
  3. Blindly Mirroring All Layouts: Not every UI element should flip. Navigation menus, timelines, pagination, and media controls often represent chronological or physical progression. Evaluate each layout decision against reading direction vs. fixed/brand intent.
  4. Ignoring dir="auto" for Mixed/UGC Content: Rendering user-generated or API-driven text without direction isolation causes bidirectional corruption. The browser's UBA requires semantic hints (dir="auto") to resolve direction from the first strong character.
  5. Hardcoding Fonts per Locale: Forking components or using inline styles for typography breaks scalability. Use the CSS :lang() selector to apply font families, line heights, and emphasis rules based on inherited language attributes.
  6. Forgetting Form Direction Persistence: When forms accept mixed-direction input, the submitted payload loses context. Use the dirname attribute alongside dir="auto" to ensure the computed direction is included in the form submission.
  7. Overriding Browser UBA with JS: Calculating direction in JavaScript and applying inline styles or classes bypasses native browser optimizations, increases bundle size, and introduces race conditions during hydration. Rely on HTML semantics and CSS logical properties.

Deliverables

  • RTL React Refactoring Blueprint: Architecture flow for implementing DirectionProvider, migrating physical CSS to logical properties, and structuring locale-agnostic component libraries. Includes hydration-safe SSR/CSR direction sync patterns.
  • Pre-Deployment RTL Validation Checklist: 12-point verification matrix covering semantic dir/lang placement, logical property adoption, UGC isolation, form dirname handling, typography :lang() rules, and accessibility/UBA compliance testing.
  • Configuration Templates:
    • DirectionProvider.tsx (CSR hydration-safe implementation)
    • Tailwind/PostCSS logical utility configuration (ms-, me-, ps-, pe-, text-start/end)
    • Base typography stylesheet using :lang() selectors for Arabic, Hebrew, Persian, and Urdu scripts