IntlPull
Technical
15 min read

RTL Language Support: Complete Arabic & Hebrew Localization Guide

Comprehensive guide to RTL language support. Learn CSS logical properties, bidirectional text handling, mirroring UI, and testing strategies for Arabic and Hebrew.

IntlPull Team
IntlPull Team
Feb 12, 2026
On this page
Summary

Comprehensive guide to RTL language support. Learn CSS logical properties, bidirectional text handling, mirroring UI, and testing strategies for Arabic and Hebrew.

RTL Fundamentals for Developers

Right-to-left (RTL) language support is a critical requirement for reaching over 400 million Arabic speakers and 9 million Hebrew speakers worldwide, yet it remains one of the most overlooked aspects of internationalization. RTL languages fundamentally change how users interact with interfaces—text flows from right to left, UI elements mirror horizontally, and even certain icons need directional flipping. The Unicode Bidirectional Algorithm (bidi) governs how mixed LTR (left-to-right) and RTL content should be displayed, creating complexity when English words, numbers, or URLs appear within Arabic or Hebrew text. Modern CSS provides logical properties that automatically adapt layouts for RTL contexts, replacing physical properties like "left" and "right" with logical ones like "inline-start" and "inline-end". Supporting RTL requires more than text direction changes—it demands rethinking navigation patterns (back buttons point right in RTL), reconsidering icon semantics (forward arrows flip but checkmarks don't), and testing visual hierarchy assumptions. Proper RTL implementation ensures Arabic and Hebrew users experience your application as if it were built natively for their language, rather than a hastily mirrored LTR interface.

CSS Logical Properties

CSS logical properties are the foundation of proper RTL support, replacing physical directions (left/right/top/bottom) with logical ones that adapt to writing mode and direction:

CSS
1/* ❌ BAD: Physical properties don't adapt to RTL */
2.card {
3  margin-left: 20px;
4  padding-right: 15px;
5  border-left: 2px solid blue;
6  text-align: left;
7}
8
9/* ✅ GOOD: Logical properties automatically flip in RTL */
10.card {
11  margin-inline-start: 20px;  /* left in LTR, right in RTL */
12  padding-inline-end: 15px;   /* right in LTR, left in RTL */
13  border-inline-start: 2px solid blue;
14  text-align: start;          /* left in LTR, right in RTL */
15}

Complete Logical Property Mapping

Physical PropertyLogical PropertyLTR EquivalentRTL Equivalent
margin-leftmargin-inline-startmargin-leftmargin-right
margin-rightmargin-inline-endmargin-rightmargin-left
padding-leftpadding-inline-startpadding-leftpadding-right
padding-rightpadding-inline-endpadding-rightpadding-left
border-leftborder-inline-startborder-leftborder-right
left: 0inset-inline-start: 0left: 0right: 0
text-align: lefttext-align: starttext-align: lefttext-align: right

Practical Example: Navigation Bar

CSS
1.navbar {
2  display: flex;
3  padding-inline: 20px; /* Shorthand for start and end */
4  gap: 16px;
5}
6
7.navbar-logo {
8  margin-inline-end: auto; /* Pushes to start edge */
9}
10
11.navbar-menu {
12  margin-inline-start: auto; /* Pushes to end edge */
13}
14
15.dropdown {
16  position: absolute;
17  inset-inline-start: 0; /* Aligns to start edge */
18}

The dir Attribute

The dir attribute is the primary mechanism for declaring text direction in HTML:

HTML
1<!-- Document-level direction -->
2<html dir="rtl" lang="ar">
3  <head>
4    <title>موقع الويب</title>
5  </head>
6  <body>
7    <h1>مرحبا بكم</h1>
8
9    <!-- Override direction for embedded LTR content -->
10    <p>
11      البريد الإلكتروني: <span dir="ltr">user@example.com</span>
12    </p>
13
14    <!-- Auto direction detection -->
15    <input type="text" dir="auto" placeholder="ابحث...">
16  </body>
17</html>

Direction Attribute Values

  • dir="ltr": Left-to-right (English, French, Spanish, etc.)
  • dir="rtl": Right-to-left (Arabic, Hebrew, Persian, Urdu)
  • dir="auto": Browser automatically detects direction from content (useful for user-generated content)

React Example with Direction

TSX
1import { useEffect } from 'react';
2
3function App({ locale }: { locale: string }) {
4  useEffect(() => {
5    const dir = ['ar', 'he', 'fa', 'ur'].includes(locale) ? 'rtl' : 'ltr';
6    document.documentElement.dir = dir;
7    document.documentElement.lang = locale;
8  }, [locale]);
9
10  return (
11    <div>
12      <h1>{locale === 'ar' ? 'مرحبا' : 'Welcome'}</h1>
13    </div>
14  );
15}

Bidirectional Text Algorithm

The Unicode Bidirectional Algorithm (UBA) automatically handles mixed LTR and RTL content, but requires understanding for edge cases:

HTML
1<!-- Example: Arabic text with English product name -->
2<p>
3  اشتريت <!-- "I bought" in Arabic (RTL) -->
4  iPhone 15 Pro <!-- Product name in English (LTR) -->
5  من متجر <!-- "from store" in Arabic (RTL) -->
6  Apple Store <!-- Store name in English (LTR) -->
7</p>
8
9<!-- Renders as: "Apple Store من متجر iPhone 15 Pro اشتريت" -->
10<!-- Reading order (RTL): اشتريت iPhone 15 Pro من متجر Apple Store -->

Controlling Bidi with Marks

Unicode provides invisible control characters for explicit bidi control:

JavaScript
1// Left-to-Right Mark (LRM): U+200E
2// Right-to-Left Mark (RLM): U+200F
3// Left-to-Right Embedding (LRE): U+202A
4// Right-to-Left Embedding (RLE): U+202B
5// Pop Directional Formatting (PDF): U+202C
6
7const arabicWithEmail = `البريد الإلكتروني: ‪user@example.com‬`;
8// Forces email to stay LTR within RTL context
9
10// React example
11function ContactInfo({ email }: { email: string }) {
12  return (
13    <p>
14      البريد الإلكتروني: <bdi>{email}</bdi>
15    </p>
16  );
17}
18
19// <bdi> element isolates content, preventing direction spillover

Mirroring UI Elements

Elements That Should Mirror

Navigation: Back/forward buttons, breadcrumbs, pagination, sidebars:

CSS
1.back-button::before {
2  content: '←';
3  /* In RTL, this should become → */
4}
5
6/* Better approach: Use transform */
7.back-button svg {
8  transform: scaleX(1);
9}
10
11[dir="rtl"] .back-button svg {
12  transform: scaleX(-1); /* Flip horizontally */
13}

Progress indicators:

CSS
1.progress-bar {
2  background: linear-gradient(to right, blue 50%, gray 50%);
3}
4
5[dir="rtl"] .progress-bar {
6  background: linear-gradient(to left, blue 50%, gray 50%);
7}
8
9/* Better with logical properties */
10.progress-bar {
11  background: linear-gradient(to inline-end, blue 50%, gray 50%);
12}

Form layouts:

CSS
1.form-label {
2  text-align: start; /* Right in RTL */
3}
4
5.form-input {
6  padding-inline-start: 12px;
7}
8
9.form-icon {
10  inset-inline-start: 8px; /* Inside start edge of input */
11}

Elements That Should NOT Mirror

Media controls: Play/pause buttons remain the same (universal symbols)

Checkmarks and close icons: These are universal and shouldn't flip

Logos and brand elements: Keep original orientation

Clocks and calendars: Maintain standard clockwise direction

Directional content in images: Maps, diagrams, photos

CSS
1/* Prevent specific elements from mirroring */
2.no-mirror {
3  transform: scaleX(1) !important;
4}
5
6[dir="rtl"] .no-mirror {
7  transform: scaleX(1) !important; /* Explicitly prevent flip */
8}

Framework-Specific RTL Support

React with styled-components

TSX
1import styled from 'styled-components';
2
3const Button = styled.button`
4  padding-inline-start: 16px;
5  padding-inline-end: 24px;
6  margin-inline-end: 8px;
7
8  &::after {
9    content: '→';
10    margin-inline-start: 8px;
11    display: inline-block;
12    transform: scaleX(${props => props.theme.dir === 'rtl' ? -1 : 1});
13  }
14`;
15
16function App() {
17  const [locale, setLocale] = useState('en');
18  const dir = ['ar', 'he'].includes(locale) ? 'rtl' : 'ltr';
19
20  return (
21    <ThemeProvider theme={{ dir }}>
22      <Button>Next</Button>
23    </ThemeProvider>
24  );
25}

Vue 3 with Composition API

VUE
1<template>
2  <div :dir="direction">
3    <nav class="navbar">
4      <div class="logo">Logo</div>
5      <ul class="menu">
6        <li v-for="item in menuItems" :key="item.id">
7          {{ item.label }}
8        </li>
9      </ul>
10    </nav>
11  </div>
12</template>
13
14<script setup lang="ts">
15import { computed } from 'vue';
16
17const props = defineProps<{ locale: string }>();
18
19const direction = computed(() =>
20  ['ar', 'he', 'fa', 'ur'].includes(props.locale) ? 'rtl' : 'ltr'
21);
22</script>
23
24<style scoped>
25.navbar {
26  display: flex;
27  padding-inline: 20px;
28}
29
30.logo {
31  margin-inline-end: auto;
32}
33
34.menu {
35  margin-inline-start: auto;
36  display: flex;
37  gap: 16px;
38}
39</style>

Angular with RTL Service

TypeScript
1import { Injectable } from '@angular/core';
2import { BehaviorSubject } from 'rxjs';
3
4@Injectable({ providedIn: 'root' })
5export class RtlService {
6  private directionSubject = new BehaviorSubject<'ltr' | 'rtl'>('ltr');
7  direction$ = this.directionSubject.asObservable();
8
9  setDirection(locale: string) {
10    const rtlLocales = ['ar', 'he', 'fa', 'ur'];
11    const dir = rtlLocales.includes(locale) ? 'rtl' : 'ltr';
12    this.directionSubject.next(dir);
13    document.documentElement.dir = dir;
14  }
15
16  isRtl(): boolean {
17    return this.directionSubject.value === 'rtl';
18  }
19}
20
21// Component usage
22@Component({
23  selector: 'app-root',
24  template: `
25    <div [dir]="direction$ | async">
26      <app-navbar></app-navbar>
27    </div>
28  `
29})
30export class AppComponent {
31  direction$ = this.rtlService.direction$;
32
33  constructor(private rtlService: RtlService) {}
34}

Flutter RTL Support

DART
1import 'package:flutter/material.dart';
2
3class MyApp extends StatelessWidget {
4  
5  Widget build(BuildContext context) {
6    return MaterialApp(
7      locale: Locale('ar'),
8      supportedLocales: [
9        Locale('en'),
10        Locale('ar'),
11        Locale('he'),
12      ],
13      localizationsDelegates: [
14        GlobalMaterialLocalizations.delegate,
15        GlobalWidgetsLocalizations.delegate,
16      ],
17      // Flutter automatically handles RTL when locale is RTL
18      home: Directionality(
19        textDirection: TextDirection.rtl,
20        child: Scaffold(
21          appBar: AppBar(title: Text('مرحبا')),
22          body: Padding(
23            padding: EdgeInsetsDirectional.only(start: 16.0),
24            child: Text('محتوى'),
25          ),
26        ),
27      ),
28    );
29  }
30}

Number Display in RTL

Numbers in RTL languages have special rules:

JavaScript
1// Arabic uses Eastern Arabic numerals (١٢٣٤٥) in some regions
2// but Western Arabic numerals (12345) in others
3
4const number = 12345;
5
6// Arabic (Saudi Arabia) - uses Western numerals
7new Intl.NumberFormat('ar-SA').format(number); // "12,345"
8
9// Arabic (Egypt) - uses Western numerals
10new Intl.NumberFormat('ar-EG').format(number); // "12,345"
11
12// Persian (Iran) - uses Eastern numerals
13new Intl.NumberFormat('fa-IR').format(number); // "۱۲٬۳۴۵"
14
15// Hebrew - uses Western numerals
16new Intl.NumberFormat('he-IL').format(number); // "12,345"

Number Direction in Mixed Text

HTML
1<!-- Numbers are always LTR, even in RTL text -->
2<p dir="rtl">
3  السعر: 299 ريال
4  <!-- Renders as: "ريال 299 :السعر" -->
5  <!-- The "299" stays LTR within RTL context -->
6</p>
7
8<!-- Use LRM to prevent numbers from affecting punctuation -->
9<p dir="rtl">
10  السعر (299‎ ريال)
11  <!-- LRM after 299 prevents parenthesis from flipping -->
12</p>

Testing RTL Layouts

Browser DevTools Testing

All modern browsers support RTL testing in DevTools:

JavaScript
1// Chrome DevTools Console
2document.documentElement.dir = 'rtl';
3
4// Firefox DevTools Console
5document.dir = 'rtl';
6
7// Quick toggle for testing
8document.dir = document.dir === 'rtl' ? 'ltr' : 'rtl';

Automated Testing with Playwright

TypeScript
1import { test, expect } from '@playwright/test';
2
3test.describe('RTL Layout', () => {
4  test('should mirror navigation in RTL', async ({ page }) => {
5    await page.goto('/');
6    await page.evaluate(() => {
7      document.documentElement.dir = 'rtl';
8    });
9
10    const backButton = page.locator('.back-button');
11    const transform = await backButton.evaluate(
12      el => window.getComputedStyle(el).transform
13    );
14
15    expect(transform).toContain('scaleX(-1)');
16  });
17
18  test('should align text to right in RTL', async ({ page }) => {
19    await page.goto('/?lang=ar');
20
21    const heading = page.locator('h1');
22    const textAlign = await heading.evaluate(
23      el => window.getComputedStyle(el).textAlign
24    );
25
26    expect(textAlign).toBe('right');
27  });
28});

Visual Regression Testing

TypeScript
1import { test } from '@playwright/test';
2
3test.describe('RTL Visual Regression', () => {
4  test('homepage LTR vs RTL', async ({ page }) => {
5    // Capture LTR
6    await page.goto('/');
7    await page.screenshot({ path: 'screenshots/homepage-ltr.png' });
8
9    // Capture RTL
10    await page.goto('/?lang=ar');
11    await page.screenshot({ path: 'screenshots/homepage-rtl.png' });
12
13    // Manual or automated comparison
14  });
15});

Common RTL Bugs Checklist

  • ✅ Text aligns to start (right in RTL)
  • ✅ Margins and padding flip correctly
  • ✅ Navigation elements mirror (back/forward buttons)
  • ✅ Forms align properly (labels, inputs, icons)
  • ✅ Dropdowns open in correct direction
  • ✅ Tooltips appear on correct side
  • ✅ Modals and dialogs center correctly
  • ✅ Scrollbars appear on correct side
  • ✅ Breadcrumbs flow right-to-left
  • ✅ Tables maintain proper column order
  • ✅ Numbers display correctly in mixed text
  • ✅ Icons that should flip do flip
  • ✅ Icons that shouldn't flip don't flip

IntlPull's RTL Preview

IntlPull provides real-time RTL preview for all translations:

  • Live Toggle: Switch between LTR and RTL instantly to see how translations render
  • Screenshot Context: View screenshots in both LTR and RTL modes
  • Length Validation: Detect overflow issues caused by RTL text expansion
  • Bidi Detection: Warnings for potential bidirectional text issues
  • Character Validation: Flag incorrect use of LTR punctuation in RTL text

When translating to Arabic or Hebrew in IntlPull, translators see a side-by-side comparison of how their translation appears in RTL context, reducing errors and improving quality.

FAQ

Q: Do all RTL languages use the same direction rules? A: Yes, Arabic, Hebrew, Persian, and Urdu all follow RTL text direction, but they may differ in number formats, punctuation, and character sets.

Q: Should I flip all icons in RTL? A: No. Only directional icons (arrows, back buttons, navigation) should flip. Universal symbols (checkmarks, close buttons, play/pause) should not.

Q: How do I handle mixed LTR/RTL content like usernames or emails? A: Use the <bdi> HTML element or Unicode bidi control characters to isolate LTR content within RTL context.

Q: Do I need separate stylesheets for RTL? A: No. Use CSS logical properties throughout your codebase. Modern CSS handles RTL automatically when dir="rtl" is set.

Q: How do I test RTL if I don't speak Arabic or Hebrew? A: Use pseudo-localization with RTL characters, automated visual regression testing, and IntlPull's RTL preview. Have native speakers review final implementations.

Q: What about vertical writing modes (Chinese, Japanese)? A: CSS logical properties also support vertical writing modes (writing-mode: vertical-rl). The same principles apply with block/inline logical properties.

Q: Should scrollbars appear on the left in RTL? A: Yes, in RTL layouts, scrollbars typically appear on the left side. Most browsers handle this automatically when dir="rtl" is set on the document.

Tags
rtl
arabic
hebrew
bidi
css
localization
right-to-left
layout
IntlPull Team
IntlPull Team
Engineering

Building tools to help teams ship products globally. Follow us for more insights on localization and i18n.