cd ../writing
// rtl · arabic typography

The honest guide to RTL CSS.

Every "RTL support" tutorial stops at direction: rtl and walks away. That's the easy 5% of the problem. The hard 95% — logical properties, bidi text, Arabic-Indic numerals, font fallback chains, mixed-script forms — is what makes Arabic UIs actually feel native. Here's the complete field guide.

7 sections Arabic + Hebrew + Persian © apply to any RTL language

I'm Egyptian. I've built three production Arabic UIs, and I've seen the same set of mistakes in every codebase claiming "RTL support". The fix isn't one CSS property — it's a whole mental shift away from thinking in left/right and toward thinking in start/end.

This guide is opinionated. I'll tell you what to use, what to ignore, and where the platform genuinely fails you and needs JS workarounds. By the end you'll be able to ship Arabic surfaces that read like a native Arabic app, not a Google-Translated English one.

01Stop using left and right. Use start and end.

Logical properties are the single most important RTL fix in modern CSS. Instead of margin-left, use margin-inline-start. Instead of border-right, use border-inline-end. The browser flips them automatically when direction: rtl is active.

✗ breaks on RTL
.card {
  padding-left: 16px;     /* always physically left */
  border-right: 1px solid;
  margin-left: auto;
  text-align: left;
}
✓ works for both LTR and RTL
.card {
  padding-inline-start: 16px;   /* flips to right on RTL */
  border-inline-end: 1px solid;
  margin-inline-start: auto;
  text-align: start;
}

The mapping is mechanical:

  • leftinline-start
  • rightinline-end
  • topblock-start
  • bottomblock-end

Same for borders, padding, margin, and even the inset shorthand (use inset-inline-start / inset-inline-end for positioning).

One exception: when you genuinely mean "physically left" — e.g., an arrow icon that always points the same direction regardless of language — keep left. Logical properties are for layout that should flip; physical properties are for things that shouldn't.

02Arabic-Indic numerals vs Western numerals

Arabic-speaking countries use two number systems. Arabic-Indic (٠١٢٣٤٥٦٧٨٩) is used in much of the Mashriq — Egypt, Saudi Arabia, the Gulf. Western Arabic (0123456789) is used in the Maghreb — Morocco, Tunisia, Algeria — and increasingly across the region for technical contexts.

Your choice depends on audience. Numerical context matters too: prices in Saudi Arabia usually use Arabic-Indic; dates in Lebanon often use Western; phone numbers in Egypt vary.

CSS gives you font-feature-settings to control numeral rendering, but the more reliable approach is font-variant-numeric or — most reliable — using a font that ships with the right numerals built in. Cairo, IBM Plex Arabic, and Tajawal all support both via OpenType features.

✓ force Arabic-Indic numerals
.price {
  font-family: 'Cairo', system-ui, sans-serif;
  font-feature-settings: 'ss01' on;   /* font-specific */
}
/* Or in JS, force conversion */
function toArabicNumerals(s) {
  return s.replace(/[0-9]/g, d => '٠١٢٣٤٥٦٧٨٩'[d]);
}
// rendered comparison
السعر: ٢٤٥ ج.م
Price: 245 EGP
Don't auto-convert input fields. If a user types 245 in an Arabic form, leave it. Only convert display numbers (prices, dates, counts). Forcing input conversion confuses users and breaks copy/paste.

03Bidirectional text — when LTR and RTL meet

The hardest part of RTL is when Arabic and English coexist in the same line. "اطلب iPhone 16 Pro من Apple" contains four direction changes. The browser uses the Unicode Bidirectional Algorithm to figure out which character goes where — and it gets some cases wrong.

Specifically, the algorithm fails on:

  • Numbers next to neutral characters like parentheses or dashes. "(2024)" in Arabic text can flip to "(4202)".
  • URLs and emails embedded in Arabic. "زور amr@iamr.net" can render the email inside-out.
  • Mixed punctuation at the end of Arabic sentences — periods and commas can land on the wrong side.

The fix is <bdi> (BiDi Isolate) — an HTML element that tells the algorithm "treat this content as its own bidi context, don't merge it with the surrounding text."

✗ URL flips on RTL
<p>زور amr@iamr.net للمزيد</p>
✓ URL stays correct
<p>زور <bdi>amr@iamr.net</bdi> للمزيد</p>

Wrap any embedded LTR content — URLs, emails, code, hashtags, English brand names, technical strings — in <bdi>. It's invisible to the user, costs zero performance, and fixes 95% of mixed-script rendering bugs.

04The Arabic font fallback chain

Don't list one Arabic font and hope. Build a chain that degrades gracefully across operating systems. Each OS ships different Arabic fonts at different quality levels.

The hierarchy I use in production:

✓ production fallback stack
body {
  font-family:
    'Cairo',                         /* Google Fonts, free, broad weights */
    'IBM Plex Arabic',                /* very technical, premium feel */
    'SF Arabic',                      /* iOS / macOS native */
    'Segoe UI Arabic',                /* Windows native */
    'Tahoma',                         /* old Windows fallback */
    system-ui, sans-serif;
}

Quick guide to picking the primary:

  • Cairo — friendly, modern, widely loved. Default for consumer apps.
  • Tajawal — neutral, professional. Good for editorial or fintech.
  • IBM Plex Arabic — technical, structured. For developer-facing or premium B2B.
  • Rubik — slightly rounded, casual. Good for playful brands.
  • Amiri — classical Naskh, serif-equivalent. Books, religious content, formal documents.
Don't use Helvetica or Arial for Arabic. They were designed for Latin scripts; their Arabic glyphs are afterthoughts. The result reads as cheap and dated. Almost any dedicated Arabic font beats them.

05Line height needs more breathing room

Arabic letters have descenders, ascenders, and stacked diacritics (tashkeel) that extend well above and below the baseline. line-height: 1.5 works for English. For Arabic, you want 1.7–1.9 minimum.

✓ Arabic-friendly line height
body {
  line-height: 1.6;
}
html[dir="rtl"] body {
  line-height: 1.85;
}
/* Or, target Arabic content specifically */
.ar { line-height: 1.85; }

Same logic for letter-spacing: don't apply tracking to Arabic. Arabic letters connect to each other; adding letter-spacing breaks the visual connection and the text reads as "broken pieces" instead of words.

06Flexbox + RTL: it mostly just works

Flexbox understands direction: rtl. flex-direction: row automatically becomes right-to-left when the container is RTL. justify-content: flex-start aligns to the inline-start (which is right in RTL). You almost don't need to think about it.

The exceptions:

  • Icon order — chevrons, arrows, and back buttons need to flip manually. A "back" arrow pointing left in English needs to point right in Arabic. Use transform: scaleX(-1) on the SVG itself, or ship two SVGs.
  • Animationstransform: translateX(20px) moves right physically regardless of dir. For RTL-aware animations, use margin-inline-start transitions or compute the direction in JS.
  • Scroll position — RTL scrolling has historic browser inconsistency. Modern Safari, Chrome, Firefox all behave (the standard now mandates "scroll position 0 = inline-start"), but legacy code may still rely on negative scroll values.
✓ icon mirroring
/* Mirror icons in RTL contexts */
[dir="rtl"] .icon-back,
[dir="rtl"] .icon-arrow,
[dir="rtl"] .icon-chevron {
  transform: scaleX(-1);
}
/* But don't mirror universal symbols */
[dir="rtl"] .icon-checkmark,
[dir="rtl"] .icon-warning,
[dir="rtl"] .icon-heart {
  transform: none;   /* these don't have a direction */
}

07Forms — the highest-stakes RTL surface

Forms break in RTL more than any other component. Input fields, validation errors, placeholder text, password reveal toggles, dropdown chevrons — every one has direction-aware behavior that needs care.

Input field text direction

Setting dir="rtl" on the input is not enough. For inputs that hold specifically Latin content (emails, URLs, technical strings), force them to LTR even inside an RTL form:

✓ form with mixed direction
<form dir="rtl">
  <!-- Arabic name field — uses form direction -->
  <input name="name" placeholder="الاسم الكامل" />
  
  <!-- Email — force LTR -->
  <input name="email" dir="ltr" placeholder="name@email.com" />
  
  <!-- Phone — force LTR for international format -->
  <input name="phone" dir="ltr" placeholder="+20 100 000 0000" />
</form>

Dropdown chevrons and icons

Chevrons inside select-style triggers should stay in their natural position — a chevron pointing down means "expand". Don't flip these.

But the chevron's horizontal placement in the button is direction-dependent. In LTR it's on the right; in RTL it should be on the left. Use logical properties:

✓ chevron at inline-end
.select {
  padding-inline-end: 36px;   /* space for chevron */
  background-image: url('chevron.svg');
  background-repeat: no-repeat;
  background-position: end center;   /* logical! */
  background-size: 14px;
}

Modern CSS understands start / end in background-position. The browser flips it for RTL automatically.

The mental shift

Switching from "left/right" thinking to "start/end" thinking takes about two weeks of writing CSS. After that you stop noticing — and your code becomes language-agnostic by default.

The payoff is concrete: an Arabic user opens your app, and it doesn't feel like an English app that got translated. Buttons sit where they expect. Numbers render in their script. Names and emails don't fight the bidi algorithm. The product feels designed for them, not localized for them.

That difference is worth two weeks of mental retraining.