# RTL Support Without a Rewrite: Arabic and Hebrew with CSS Logical Properties

> Supporting right-to-left languages does not mean maintaining a second stylesheet. Learn how CSS logical properties, the dir attribute, and a few Tailwind patterns let one codebase mirror itself correctly for Arabic, Hebrew, Persian, and Urdu.
- **Author**: Mohit Sapat
- **Published**: 2026-09-29
- **URL**: https://www.babelize.co/blogs/rtl-support-css-logical-properties

---

<p class="lead">
    Supporting right-to-left languages does not mean maintaining a second stylesheet. Learn how CSS logical properties, the <code>dir</code> attribute, and a few Tailwind patterns let one codebase mirror itself correctly for Arabic, Hebrew, Persian, and Urdu.
</p>

<h2>Why RTL Keeps Getting Postponed</h2>

<p>
    Arabic (<code>ar</code>), Hebrew (<code>he</code>), Persian (<code>fa</code>), and Urdu (<code>ur</code>), identified by their <a href="/blogs/locale-codes-bcp-47-explained/">BCP 47 locale codes</a>, are read right to left, and together they cover well over half a billion people. Arabic alone is one of the most widely spoken languages in the world, and markets such as the Gulf states are an increasingly common target for SaaS products.
</p>

<p>
    RTL still gets deferred on most roadmaps, because the received wisdom is that it means a second stylesheet, a build-time flipping step, and a permanent maintenance tax. That was accurate around 2015. It is not accurate now. With CSS logical properties -- supported in every current browser -- the browser does the mirroring for you, and the work becomes a mostly mechanical find-and-replace plus a handful of genuine judgment calls.
</p>

<h2>Step 1: Set the Direction on the Document</h2>

<p>
    Everything downstream depends on one attribute:
</p>

<pre><code>&lt;html lang="ar" dir="rtl"&gt;</code></pre>

<p>
    Set it server-side from the active locale (if you are on Next.js, the <code>[locale]</code> layout from our <a href="/blogs/implement-i18n-nextjs-next-intl/">next-intl setup guide</a> is the natural place). Two reasons not to do it in a <code>useEffect</code>: the first paint renders in the wrong direction and visibly flips, and any server-rendered HTML you send is wrong until hydration.
</p>

<pre><code>// app/[locale]/layout.tsx
const RTL_LOCALES = ["ar", "he", "fa", "ur"];

export default async function Layout({ children, params }) {
  const { locale } = await params;
  const dir = RTL_LOCALES.includes(locale) ? "rtl" : "ltr";

  return (
    &lt;html lang={locale} dir={dir}&gt;
      &lt;body&gt;{children}&lt;/body&gt;
    &lt;/html&gt;
  );
}</code></pre>

<p>
    Setting <code>dir</code> alone already fixes text alignment, the direction text flows within a paragraph, and the default order of inline elements. It does not fix any layout you pinned with <code>left</code>, <code>right</code>, <code>margin-left</code>, or <code>padding-right</code>. That is step two.
</p>

<h2>Step 2: Replace Physical Properties with Logical Ones</h2>

<p>
    CSS has two coordinate systems. Physical properties (<code>left</code>, <code>right</code>, <code>top</code>, <code>bottom</code>) refer to the screen. Logical properties refer to the <em>text flow</em>: the <strong>inline</strong> axis runs in the reading direction, the <strong>block</strong> axis runs down the page. In an LTR document <code>inline-start</code> resolves to left; in an RTL document it resolves to right. Same CSS, mirrored result.
</p>

<table>
    <thead>
        <tr>
            <th>Physical</th>
            <th>Logical</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>margin-left / margin-right</td>
            <td>margin-inline-start / margin-inline-end</td>
        </tr>
        <tr>
            <td>padding-left / padding-right</td>
            <td>padding-inline-start / padding-inline-end</td>
        </tr>
        <tr>
            <td>left / right</td>
            <td>inset-inline-start / inset-inline-end</td>
        </tr>
        <tr>
            <td>border-left</td>
            <td>border-inline-start</td>
        </tr>
        <tr>
            <td>text-align: left / right</td>
            <td>text-align: start / end</td>
        </tr>
        <tr>
            <td>border-radius corners</td>
            <td>border-start-start-radius, etc.</td>
        </tr>
        <tr>
            <td>width / height</td>
            <td>inline-size / block-size</td>
        </tr>
    </tbody>
</table>

<p>
    A concrete before and after -- a sidebar with a left border and an offset icon:
</p>

<pre><code>/* Before -- breaks in RTL */
.callout {
  border-left: 3px solid var(--accent);
  padding-left: 1rem;
  margin-left: 2rem;
  text-align: left;
}

/* After -- mirrors automatically */
.callout {
  border-inline-start: 3px solid var(--accent);
  padding-inline-start: 1rem;
  margin-inline-start: 2rem;
  text-align: start;
}</code></pre>

<p>
    Note that <code>margin-block</code> and <code>padding-block</code> exist too, but vertical values do not change under RTL -- so converting them is stylistic consistency, not a bug fix. Prioritize the inline axis.
</p>

<h3>If you use Tailwind</h3>

<p>
    Tailwind v3.3+ ships logical utilities and they are drop-in replacements:
</p>

<pre><code>ml-4  ->  ms-4       (margin-inline-start)
mr-4  ->  me-4       (margin-inline-end)
pl-6  ->  ps-6       (padding-inline-start)
pr-6  ->  pe-6       (padding-inline-end)
left-0 ->  start-0
right-0 ->  end-0
text-left ->  text-start
text-right ->  text-end
border-l ->  border-s</code></pre>

<p>
    For the rare case that genuinely needs direction-specific styling, Tailwind also has <code>rtl:</code> and <code>ltr:</code> variants:
</p>

<pre><code>&lt;ChevronRight className="rtl:rotate-180" /&gt;</code></pre>

<p>
    Use those variants sparingly. Every one is a place where the two directions can drift apart. If you find yourself writing more than a handful, the underlying layout probably still uses physical properties.
</p>

<h2>Step 3: The Things That Must Not Flip</h2>

<p>
    Mirroring is not universal, and over-mirroring looks just as broken as under-mirroring.
</p>

<h3>Numbers and Latin text inside RTL</h3>

<p>
    Arabic text containing a phone number, a version string, or an English product name uses the Unicode bidirectional algorithm to lay out those runs left to right inside a right-to-left paragraph. The browser handles this for you as long as you do not fight it. Where it goes wrong is user-generated content that mixes scripts -- wrap untrusted mixed content in an element with <code>dir="auto"</code> so the browser infers direction from the first strong character. Mixed-script strings bring other traps too, covered in <a href="/blogs/unicode-string-handling-javascript-i18n/">Unicode string bugs that break i18n</a>.
</p>

<h3>Code blocks and terminal output</h3>

<p>
    Code is always LTR, in every locale. Force it:
</p>

<pre><code>pre, code, kbd, samp {
  direction: ltr;
  text-align: left;
}</code></pre>

<p>
    Forgetting this produces right-aligned, visually reordered code samples that are genuinely unreadable.
</p>

<h3>Icons: mirror the directional, keep the representational</h3>

<ul>
    <li><strong>Mirror:</strong> back and forward arrows, next/previous chevrons, undo and redo, progress indicators, send arrows, indentation controls.</li>
    <li><strong>Do not mirror:</strong> clocks (time still runs clockwise), play buttons on media (a long-standing convention that stays pointing right), checkmarks, logos, magnifying glasses, most object icons.</li>
    <li><strong>Judgment call:</strong> a pencil or hand-writing icon -- usually left alone.</li>
</ul>

<p>
    The usable rule: if the icon means "that way," mirror it. If it depicts a thing, leave it.
</p>

<h3>Charts, timelines, and progress bars</h3>

<p>
    A progress bar filling from the left in an RTL interface reads as emptying. Anchor fills and animations to <code>inset-inline-start</code>. Chart axes are a real design decision -- many RTL products keep time-series charts running left to right because the convention is borrowed from math, but menus, steppers, and timelines should follow the reading order.
</p>

<h2>Step 4: Typography</h2>

<p>
    Arabic and Hebrew are not just LTR text pointed the other way. Practical notes:
</p>

<ul>
    <li><strong>Line height needs more room.</strong> Arabic has tall ascenders and deep descenders. A <code>line-height</code> of 1.5 that looks fine in Latin often looks cramped in Arabic; 1.7-1.8 reads better.</li>
    <li><strong>Pick a real Arabic font.</strong> Most Latin webfonts have no Arabic glyphs, so the browser silently falls back to a system font and your carefully chosen type disappears. Noto Sans Arabic, IBM Plex Sans Arabic, and Cairo are solid open options -- declare them in the font stack for RTL locales.</li>
    <li><strong>Arabic does not have letter case or italics.</strong> <code>text-transform: uppercase</code> is a no-op and synthetic italics look wrong. Use weight and color for emphasis instead.</li>
    <li><strong>Letter-spacing breaks Arabic.</strong> Arabic letters connect; adding tracking literally severs the connections. Zero it out for RTL locales.</li>
    <li><strong>Text expands.</strong> Arabic is often shorter than English, Hebrew similar, but both vary enough that fixed-width buttons will overflow. Test with real strings, not Lorem Ipsum.</li>
</ul>

<h2>Testing Without Speaking Arabic</h2>

<p>
    You do not need a translator to catch layout bugs. Force the direction and look at the page:
</p>

<pre><code>// In devtools console -- instant RTL preview of any page
document.documentElement.dir = "rtl";</code></pre>

<p>
    What to look for: elements that did not move when everything else did. A misaligned close button, a dropdown that opens off-screen, an avatar still pinned to the left of a right-aligned name, a tooltip arrow pointing at nothing. Each one is a physical property you missed.
</p>

<p>
    Worth adding to CI: a couple of visual regression snapshots taken with <code>dir="rtl"</code>, plus a lint rule banning <code>margin-left</code>, <code>padding-right</code>, and friends in new CSS. Preventing regressions is much cheaper than a second audit in six months. A <a href="/blogs/pseudo-localization-testing-guide/">pseudo-locale</a> that also flips direction lets you run the same checks on every pull request.
</p>

<h2>Frequently Asked Questions</h2>

<h3>How do I add RTL support to a web app?</h3>
<p>
    Set <code>dir="rtl"</code> on the html element for right-to-left locales and replace directional CSS properties with logical equivalents, such as <code>margin-inline-start</code> instead of <code>margin-left</code> and <code>text-align: start</code> instead of <code>text-align: left</code>. The browser then mirrors the layout automatically without a second stylesheet.
</p>

<h3>What are CSS logical properties?</h3>
<p>
    Logical properties describe position relative to text flow rather than to the screen. The inline axis follows the reading direction, so <code>inline-start</code> resolves to left in a left-to-right document and to right in a right-to-left one, letting the same CSS serve both directions.
</p>

<h3>Which icons should be mirrored in RTL layouts?</h3>
<p>
    Mirror icons that indicate direction, such as back and forward arrows, next and previous chevrons, undo and redo, and send arrows. Do not mirror icons that depict objects, such as clocks, checkmarks, magnifying glasses, logos, or media play buttons.
</p>

<h3>Does Tailwind CSS support RTL?</h3>
<p>
    Yes. Tailwind provides logical utilities such as <code>ms-4</code>, <code>me-4</code>, <code>ps-6</code>, <code>pe-6</code>, <code>start-0</code>, <code>end-0</code>, and <code>text-start</code> that map to CSS logical properties, plus <code>rtl:</code> and <code>ltr:</code> variants for the rare cases that genuinely need direction-specific styling.
</p>

<h3>How can I test RTL without speaking Arabic?</h3>
<p>
    Run <code>document.documentElement.dir = "rtl"</code> in the browser console to flip any page instantly. Then look for elements that did not move with the rest of the layout, such as misaligned close buttons, off-screen dropdowns, or tooltip arrows pointing at nothing.
</p>

<h2>The Short Version</h2>

<p>
    RTL is a one-time conversion, not an ongoing second codebase. Set <code>dir</code> on the html element from the locale, convert physical CSS properties to logical ones, explicitly pin code blocks to LTR, mirror only the icons that indicate direction, and give Arabic proper fonts and line height. After that, adding Hebrew or Persian is only a translation task.
</p>

<p>
    And once the layout mirrors correctly, shipping Arabic becomes a content problem rather than an engineering one -- which is where <a href="/">Babelize</a> fits, keeping your RTL locale files in sync with every new string you add instead of letting Arabic fall three releases behind English.
</p>
---
- [All articles](https://www.babelize.co/blogs)