Articles › CSS & Web Design

Building RTL and Bilingual Websites: A Practical Guide for Persian and Arabic

Build professional right-to-left and bilingual (RTL/LTR) websites: dir and lang, CSS logical properties, mixed-direction text, bidi bugs, fonts, numbers, dates, icons and SEO with hreflang.

Hundreds of millions of people read right-to-left: Persian, Arabic, Hebrew and Urdu speakers. Building a website that feels native in RTL — and switches cleanly to English — is more than adding direction: rtl. This guide collects the lessons learned building this very site, which serves Persian and English from the same codebase.

1. Start with dir and lang on the html element

<html lang="fa" dir="rtl">
  • dir="rtl" flips the base direction: text alignment, flex and grid flow, table column order, scrollbar position.
  • lang="fa" lets browsers choose correct fonts, hyphenation, quotes and screen-reader pronunciation — and helps search engines.

In a bilingual React app, update both whenever the language changes:

useEffect(() => {
  document.documentElement.lang = lang;
  document.documentElement.dir = lang === "fa" ? "rtl" : "ltr";
}, [lang]);

Prefer dir over the CSS direction property — it carries semantic meaning and is respected by the bidi algorithm everywhere.

2. Use CSS logical properties

Physical properties (left, right, margin-left) don't flip. Logical properties follow the writing direction automatically:

Physical Logical
margin-left / margin-right margin-inline-start / margin-inline-end
padding-left padding-inline-start
left: 0 inset-inline-start: 0
border-right border-inline-end
text-align: left text-align: start
top / bottom inset-block-start / inset-block-end
.card__badge {
  position: absolute;
  inset-block-start: 12px;
  inset-inline-start: 12px;   /* top-left in English, top-right in Persian */
}

Flexbox and Grid already follow dir: justify-content: flex-start means right in RTL. With logical properties, one stylesheet serves both languages — no separate rtl.css.

3. Directional icons and transforms

Arrows, chevrons, "back" and "next" icons must mirror; logos, media controls (play), checkmarks and brand icons must not.

[dir="rtl"] .flip-rtl {
  transform: scaleX(-1);
}

Transforms and gradients aren't logical either. translateX(100%) moves right in both directions, and linear-gradient(90deg, …) always goes left-to-right. Handle them explicitly:

.progress { transform-origin: left; }
[dir="rtl"] .progress { transform-origin: right; }

4. Mixed-direction text and the bidi algorithm

Persian technical text constantly mixes in English words: "ASP.NET Core", "React", "C#". The Unicode bidirectional algorithm handles most cases, but a few traps are common.

Trap 1: neutral characters at the edges

In an RTL paragraph, C# followed by a Persian word often renders as #C, because # is a neutral character and takes the paragraph's direction. Fix it with an invisible LRM (U+200E) after the symbol, or wrap the term in <bdi> / <span dir="ltr">:

<p>تخصص من <bdi>C#</bdi> و ASP.NET Core است.</p>

Trap 2: word-by-word animations

Text animation libraries often wrap each word in an inline-block span. Each span becomes an isolated unit for the bidi algorithm, so an English phrase inside Persian text appears reversed: "Core ASP.NET". Solutions: split Persian text by lines instead of words, or group consecutive Latin words into a single unit with dir="ltr" before animating.

Trap 3: user-generated content

For content whose direction you don't know (usernames, comments, search results), let the browser decide per element:

<span dir="auto">{comment.text}</span>

Also use unicode-bidi: isolate (or <bdi>) for inline values like usernames inside sentences, so they don't scramble the surrounding text.

5. Code, URLs, emails and phone numbers

These are always LTR, even inside RTL text:

<a href="mailto:info@example.com" dir="ltr">info@example.com</a>
<span dir="ltr">0912 345 6789</span>
<pre dir="ltr"><code>var x = 42;</code></pre>

In articles, make every code block dir="ltr" with text-align: left. Persian comments inside code also need a font that contains Persian glyphs — add a Persian font after the monospace font in the font-family stack, otherwise letters may render disconnected.

6. Fonts and typography

  • Use a high-quality Persian font such as Vazirmatn (variable, open source) and self-host it for performance and reliability.
  • Load different font stacks per language: Persian text looks wrong in Latin-only fonts, and English often looks better in a dedicated Latin font.
:root[lang="fa"] { --font: "Vazirmatn Variable", Tahoma, sans-serif; }
:root[lang="en"] { --font: "Inter Variable", system-ui, sans-serif; }
body { font-family: var(--font); }
  • Persian needs more line height (≈1.9–2.1 for body text) than English (≈1.6–1.8).
  • Avoid negative letter-spacing in Persian — it breaks the connected script.
  • Use the ZWNJ (zero-width non-joiner, نیم‌فاصله) correctly in Persian copy: «می‌شود», «کتاب‌ها».

7. Numbers and dates

new Intl.NumberFormat("fa-IR").format(1250000);           // ۱٬۲۵۰٬۰۰۰
new Intl.DateTimeFormat("fa-IR-u-ca-persian", {
  day: "numeric", month: "long", year: "numeric",
}).format(new Date());                                      // ۲ تیر ۱۴۰۵
new Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format(new Date()); // June 23, 2026
  • Use the built-in Intl API instead of hand-written converters.
  • Store dates in UTC; format them per language at display time.
  • Accept both Persian and Latin digits in inputs (users type either) and normalize before validation.

8. Forms and inputs

  • Phone, email, URL and code fields should be dir="ltr" even on Persian pages.
  • Placeholders and labels follow the page language.
  • Validation messages must be translated — and avoid concatenating sentences, since word order differs between languages.

9. Bilingual architecture

  • Put the language in the URL: /fa/articles/… and /en/articles/…. It's shareable, cacheable and indexable.
  • Keep UI strings in dictionaries; keep content translations as separate fields (title, titleEn) or tables.
  • Remember the user's choice (cookie/localStorage); for first visits, you can default by IP country or browser language — but always offer a visible language switch.
  • Fall back gracefully when a translation is missing.

10. SEO for multilingual sites

Tell search engines how pages relate:

<link rel="canonical" href="https://example.com/fa/articles/rtl-guide" />
<link rel="alternate" hreflang="fa" href="https://example.com/fa/articles/rtl-guide" />
<link rel="alternate" hreflang="en" href="https://example.com/en/articles/rtl-guide" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/articles/rtl-guide" />
  • Each language version has its own URL and canonical.
  • hreflang pairs must be reciprocal.
  • Don't automatically redirect search engine bots based on IP for every URL — language-specific URLs must stay reachable.
  • Translate titles, meta descriptions and structured data (inLanguage), not just body text.
  • Include all language versions in your sitemap.

Testing checklist

  • Switch languages on every page type; check alignment, icons, animations.
  • Look for reversed English phrases and #C-style bugs in Persian text.
  • Check forms: phone and email fields, validation messages.
  • Verify dates, numbers and currency formats in both languages.
  • Inspect the HTML for lang, dir, hreflang and canonical tags.

Summary

Professional RTL and bilingual sites rely on a few principles: set dir and lang, write CSS with logical properties, mirror only directional icons, respect the bidi algorithm (isolate LTR terms, avoid word-splitting Persian), choose proper fonts and line heights, format numbers and dates with Intl, put the language in the URL, and connect versions with hreflang. Get these right and your site will feel native in every language.

FAQ

Do I need a separate RTL stylesheet?

Not if you use logical properties. A few direction-specific overrides (transforms, gradients, icons) are enough.

Should Persian sites use Persian digits?

In body text and UI, yes — it feels native. In code, IDs and some technical values, Latin digits are expected.

Is redirecting users by IP good for SEO?

Redirecting only the root URL is fine if every language has its own crawlable URL and you provide a manual switch.