TL;DR: Supporting Arabic, Hebrew, Persian, and Urdu requires layout mirroring. This guide covers CSS logical properties,
dir="rtl"handling, third-party component adaptation, and testing RTL layouts. RTL patterns from tanstackship.com.
Introduction
Over 400 million people read from right to left. Supporting RTL languages is essential for Middle Eastern, South Asian, and North African markets. RTL support involves more than text direction — it affects layout, icons, animations, and form interactions. For foundational i18n architecture, see our i18n for SaaS: Architecture Patterns Compared.
RTL Implementation Approaches
| Approach | Effort | Reliability | Maintainability |
|---|---|---|---|
| CSS logical properties | Low | Excellent | High |
dir attribute + CSS flip | Low | Good | Medium |
| CSS-in-JS with RTL plugin | Medium | Good | Medium |
| PostCSS RTL plugin | Medium | Very good | High |
| Manual RTL stylesheets | High | Poor | Very low |
CSS Logical Properties (Recommended)
/* Before (physical properties — RTL breaks these) */
.sidebar {
margin-left: 16px;
padding-right: 24px;
border-left: 1px solid #e5e7eb;
text-align: left;
}
/* After (logical properties — auto-adapt to RTL) */
.sidebar {
margin-inline-start: 16px;
padding-inline-end: 24px;
border-inline-start: 1px solid #e5e7eb;
text-align: start;
}
| Physical Property | Logical Equivalent |
|---|---|
left | inset-inline-start |
right | inset-inline-end |
margin-left | margin-inline-start |
margin-right | margin-inline-end |
padding-left | padding-inline-start |
padding-right | padding-inline-end |
border-left | border-inline-start |
border-right | border-inline-end |
React RTL Context
// RTL provider component
function RTLProvider({ locale, children }: { locale: string; children: React.ReactNode }) {
const direction = getDirection(locale) // 'rtl' or 'ltr'
useEffect(() => {
document.documentElement.dir = direction
document.documentElement.lang = locale
}, [direction, locale])
return (
<ThemeProvider theme={{ direction }}>
{children}
</ThemeProvider>
)
}
// Hook for conditional RTL styles
function useRTL() {
const { i18n } = useTranslation()
const isRTL = ['ar', 'he', 'fa', 'ur'].includes(i18n.language)
return {
isRTL,
direction: isRTL ? 'rtl' : 'ltr',
// Flip icons and imagery
fliplcon: (icon: string) => isRTL ? `${icon}-flip` : icon,
// Adjust animation direction
slideDirection: isRTL ? 'left' : 'right',
}
}
Third-Party Component Compatibility
| Component Library | RTL Support | Notes |
|---|---|---|
| Radix UI | Full | Auto-detects from dir attribute |
| shadcn/ui | Full | Inherits from Radix UI |
| TanStack Table | Good | Manual column alignment needed |
| TanStack Form | Full | Respects dir attribute |
| Recharts | Partial | Axis labels need manual config |
| TipTap editor | Full | RTL via direction extension |
Testing RTL Layouts
// Playwright RTL test
test('dashboard renders correctly in Arabic', async ({ page }) => {
await page.goto('/ar/dashboard')
// Check document direction
const dir = await page.getAttribute('html', 'dir')
expect(dir).toBe('rtl')
// Check layout mirroring
const sidebarBox = await page.locator('[data-testid="sidebar"]').boundingBox()
const mainBox = await page.locator('[data-testid="main-content"]').boundingBox()
// In RTL, sidebar should be on the right
expect(sidebarBox.x + sidebarBox.width).toBeCloseTo(mainBox.x, 0)
// Visual snapshot comparison
await expect(page).toHaveScreenshot('dashboard-ar.png')
})
Conclusion
Start with CSS logical properties — they handle 90% of RTL layout automatically. Add a locale-aware RTL context provider, test with Playwright visual snapshots, and audit third-party components for RTL compatibility. Support the four major RTL languages (Arabic, Hebrew, Persian, Urdu) to access fast-growing markets.
For navigating these markets, see Middle East Market Entry Guide. Locale-aware formatting is covered in Localizing Date, Time, Number, and Currency Formats. For responsive layout patterns, our CSS Container Queries Guide provides modern alternatives.