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:
CSS1/* ❌ 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 Property | Logical Property | LTR Equivalent | RTL Equivalent |
|---|---|---|---|
margin-left | margin-inline-start | margin-left | margin-right |
margin-right | margin-inline-end | margin-right | margin-left |
padding-left | padding-inline-start | padding-left | padding-right |
padding-right | padding-inline-end | padding-right | padding-left |
border-left | border-inline-start | border-left | border-right |
left: 0 | inset-inline-start: 0 | left: 0 | right: 0 |
text-align: left | text-align: start | text-align: left | text-align: right |
Practical Example: Navigation Bar
CSS1.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:
HTML1<!-- 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
TSX1import { 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:
HTML1<!-- 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:
JavaScript1// 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:
CSS1.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:
CSS1.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:
CSS1.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
CSS1/* 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
TSX1import 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
VUE1<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
TypeScript1import { 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
DART1import '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:
JavaScript1// 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
HTML1<!-- 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:
JavaScript1// 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
TypeScript1import { 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
TypeScript1import { 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.
