Supporting RTL (Right-to-Left) Languages
RTL support involves three main areas: (1) Setting text direction at document/component level, (2) Using CSS logical properties for layout that adapts to direction, and (3) Handling component-level mirroring and bidirectional logic. The key is making your UI direction-agnostic using modern CSS and React patterns.
1. Understanding Text Direction
What is RTL?
Right-to-Left (RTL) languages read from right to left, unlike Left-to-Right (LTR) languages like English. Common RTL languages include:
Unicode Bidirectional Algorithm
Unicode automatically handles text direction using the Bidirectional Algorithm (Bidi):
The algorithm determines direction based on character properties, not just language.
Example: Mixed Content
Hello مرحبا WorldThe bidi algorithm processes this as:
1. "Hello " → LTR
2. "مرحبا" → RTL (Arabic)
3. " World" → LTR
Result: Hello مرحبا World (correctly rendered)
2. Setting Document Direction
HTML Level
Method 1: HTML dir Attribute (Recommended)
<html dir="rtl" lang="ar">
<head>
<meta charset="UTF-8">
</head>
<body>
<!-- All content flows RTL -->
</body>
</html>Method 2: CSS direction Property
html {
direction: rtl;
}Note: The dir attribute is preferred because it's semantic and accessible.
React Level
Option 1: Set on Root Element
// In your root component or _document.tsx (Next.js)
useEffect(() => {
const html = document.documentElement;
html.setAttribute('dir', 'rtl');
html.setAttribute('lang', 'ar');
return () => {
html.removeAttribute('dir');
html.removeAttribute('lang');
};
}, []);Option 2: Component-Level Direction
function RTLContainer({ children, lang = 'ar' }) {
return (
<div dir="rtl" lang={lang}>
{children}
</div>
);
}Option 3: Dynamic Direction Switching
function App() {
const [direction, setDirection] = useState<'ltr' | 'rtl'>('ltr');
useEffect(() => {
document.documentElement.setAttribute('dir', direction);
}, [direction]);
return (
<div>
<button onClick={() => setDirection(d => d === 'ltr' ? 'rtl' : 'ltr')}>
Toggle Direction
</button>
{/* Your app content */}
</div>
);
}3. CSS Logical Properties
The Problem with Physical Properties
Traditional CSS uses physical properties tied to screen position:
/* These don't adapt to RTL */
.element {
margin-left: 20px;
padding-right: 10px;
border-left: 1px solid;
text-align: left;
}| Physical | Logical (Block) | Logical (Inline) |
|---|---|---|
width | block-size | inline-size |
height | block-size | - |
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 |
left | inset-block-start | inset-inline-start |
right | inset-block-end | inset-inline-end |
text-align: left | - | text-align: start |
text-align: right | - | text-align: end |
/* ❌ Physical properties - breaks in RTL */
.card {
margin-left: 20px;
padding-right: 16px;
border-left: 2px solid blue;
}
/* ✅ Logical properties - works in both directions */
.card {
margin-inline-start: 20px; /* left in LTR, right in RTL */
padding-inline-end: 16px; /* right in LTR, left in RTL */
border-inline-start: 2px solid blue;
}Flexbox and Grid
Logical properties work with Flexbox and Grid:
/* ❌ Physical */
.container {
justify-content: flex-start; /* Always left */
}
/* ✅ Logical */
.container {
justify-content: start; /* Left in LTR, right in RTL */
}4. Layout Mirroring
What Needs to Mirror?
Not everything should mirror! Here's what typically mirrors vs. what doesn't:
Should Mirror ✅
Should NOT Mirror ❌
CSS Transform Mirroring
For elements that should mirror:
/* Mirror horizontally */
[dir="rtl"] .icon-arrow {
transform: scaleX(-1);
}
/* Or use CSS variable */
.icon-arrow {
transform: scaleX(var(--direction, 1));
}
[dir="rtl"] {
--direction: -1;
}Selective Mirroring
Use [dir="rtl"] selector for conditional mirroring:
/* Default LTR */
.nav-menu {
margin-left: auto;
}
/* RTL override */
[dir="rtl"] .nav-menu {
margin-left: 0;
margin-right: auto;
}Or use logical properties (better approach):
/* Works in both directions */
.nav-menu {
margin-inline-start: auto;
}5. Component-Level RTL Support
React Component Patterns
Pattern 1: Direction-Aware Hook
function useDirection() {
const [direction, setDirection] = useState<'ltr' | 'rtl'>('ltr');
useEffect(() => {
const html = document.documentElement;
const dir = html.getAttribute('dir') || 'ltr';
setDirection(dir as 'ltr' | 'rtl');
const observer = new MutationObserver(() => {
const newDir = html.getAttribute('dir') || 'ltr';
setDirection(newDir as 'ltr' | 'rtl');
});
observer.observe(html, { attributes: true, attributeFilter: ['dir'] });
return () => observer.disconnect();
}, []);
return direction;
}Pattern 2: RTL-Aware Styling Hook
function useRTLStyles() {
const direction = useDirection();
const isRTL = direction === 'rtl';
return {
marginStart: isRTL ? 'marginRight' : 'marginLeft',
marginEnd: isRTL ? 'marginLeft' : 'marginRight',
paddingStart: isRTL ? 'paddingRight' : 'paddingLeft',
paddingEnd: isRTL ? 'paddingLeft' : 'paddingRight',
textAlign: isRTL ? 'right' : 'left',
};
}Better: Use CSS logical properties instead of JavaScript!
Pattern 3: Conditional Icon Rendering
function ArrowIcon({ direction: dir }: { direction: 'ltr' | 'rtl' }) {
const isRTL = dir === 'rtl';
return (
<svg
style={{ transform: isRTL ? 'scaleX(-1)' : 'none' }}
viewBox="0 0 24 24"
>
<path d="M8 4l8 8-8 8" />
</svg>
);
}
// Usage
function Navigation() {
const direction = useDirection();
return (
<nav>
<ArrowIcon direction={direction} />
</nav>
);
}Pattern 4: RTL-Aware Component Wrapper
interface RTLProps {
children: React.ReactNode;
lang?: string;
}
function RTLProvider({ children, lang = 'ar' }: RTLProps) {
useEffect(() => {
const html = document.documentElement;
html.setAttribute('dir', 'rtl');
html.setAttribute('lang', lang);
return () => {
html.removeAttribute('dir');
html.removeAttribute('lang');
};
}, [lang]);
return <>{children}</>;
}6. Handling Mixed Content
Bidirectional Text Challenges
When LTR and RTL content mix, you need to handle:
1. Text with numbers: Numbers are LTR even in RTL text
2. URLs and emails: Always LTR
3. Code snippets: Usually LTR
4. Mixed paragraphs: Each paragraph can have different direction
Unicode Bidirectional Marks
Use Unicode marks to control direction explicitly:
\u200E): Left-to-Right Mark\u200F): Right-to-Left Mark\u202A): Left-to-Right Embedding\u202B): Right-to-Left Embedding\u202C): Pop Directional FormattingExample: Fixing Number Direction
// Problem: Numbers in RTL text
const text = "السعر 100 دولار"; // "Price 100 dollars"
// Solution: Add LRM after number
const text = "السعر 100 دولار"; // Forces "100" to be LTRCSS isolation Property
Use isolation: isolate to create a new stacking context and isolate bidi:
.mixed-content {
isolation: isolate;
unicode-bidi: isolate;
}React: Handling Mixed Content
function MixedContent({ arabicText, englishText }: Props) {
return (
<div dir="auto"> {/* Auto-detects direction */}
<p>{arabicText}</p>
<p dir="ltr">{englishText}</p> {/* Force LTR */}
</div>
);
}
// Or use bidi-isolate
function IsolatedContent({ content }: Props) {
return (
<span style={{ unicodeBidi: 'isolate' }}>
{content}
</span>
);
}7. Common Pitfalls and Solutions
Pitfall 1: Hardcoded Physical Properties
Problem: Using margin-left, padding-right everywhere
Solution: Use logical properties (margin-inline-start, padding-inline-end)
Pitfall 2: Assuming LTR Layout
Problem: JavaScript calculations assume left-to-right
// ❌ Assumes menu is always on left
const menuPosition = { left: 0 };Solution: Use CSS for positioning, or check direction:
// ✅ Direction-aware
const menuPosition = isRTL
? { right: 0 }
: { left: 0 };
// Better: Use CSS logical properties
const menuStyle = { insetInlineStart: 0 };Pitfall 3: Mirroring Everything
Problem: Using transform: scaleX(-1) on everything
Solution: Only mirror directional elements (arrows, chevrons), not logos or images
Pitfall 4: Not Testing with Real RTL Text
Problem: Testing with flipped English text
Solution: Always test with actual Arabic/Hebrew text
Pitfall 5: Ignoring Font Selection
Problem: Using fonts that don't support RTL characters
Solution: Use fonts with RTL support (Google Fonts, system fonts)
/* Good RTL-supporting fonts */
@import url('https://fonts.googleapis.com/css2?family=Cairo:wght@400;700&display=swap');
body {
font-family: 'Cairo', 'Arial', sans-serif;
}Pitfall 6: Breaking Functionality
Problem: Mirroring breaks interactions (dropdowns, tooltips)
Solution: Test all interactions in RTL mode
// Example: RTL-aware dropdown
function Dropdown({ isOpen, onToggle }) {
const direction = useDirection();
const isRTL = direction === 'rtl';
return (
<div className="dropdown">
<button onClick={onToggle}>Menu</button>
{isOpen && (
<div
className="dropdown-menu"
style={{
[isRTL ? 'right' : 'left']: 0, // Position based on direction
}}
>
{/* Menu items */}
</div>
)}
</div>
);
}8. Testing RTL Support
Manual Testing Checklist
dir="rtl" on HTML elementBrowser DevTools
Chrome DevTools
1. Open DevTools (F12)
2. Go to Console
3. Run: document.documentElement.setAttribute('dir', 'rtl')
4. Or use Rendering tab → Emulate CSS media feature → prefers-color-scheme
Firefox DevTools
1. Open DevTools (F12)
2. Go to Inspector
3. Right-click on <html> → Edit HTML
4. Add dir="rtl"
Automated Testing
Jest + Testing Library
import { render } from '@testing-library/react';
test('renders correctly in RTL', () => {
document.documentElement.setAttribute('dir', 'rtl');
const { container } = render(<MyComponent />);
const element = container.querySelector('.my-element');
expect(element).toHaveStyle({
marginInlineStart: '20px'
});
});Playwright
test('RTL layout', async ({ page }) => {
const htmlContent = '<html dir="rtl" lang="ar"><body>' + componentHTML + '</body></html>';
await page.setContent(htmlContent);
const menu = page.locator('.menu');
await expect(menu).toHaveCSS('margin-inline-start', 'auto');
});Visual Regression Testing
Use tools like Percy or Chromatic to compare LTR vs RTL screenshots:
// Storybook example
export const RTLVersion = {
decorators: [
(Story) => (
<div dir="rtl" lang="ar">
<Story />
</div>
),
],
};9. Design System Considerations
Token-Based Approach
Design systems should use direction-agnostic tokens:
// ❌ Bad: Direction-specific tokens
const tokens = {
spacingLeft: '16px',
spacingRight: '16px',
};
// ✅ Good: Logical tokens
const tokens = {
spacingInlineStart: '16px',
spacingInlineEnd: '16px',
};Component Library Patterns
Pattern: RTL-Aware Component Props
interface ButtonProps {
icon?: React.ReactNode;
iconPosition?: 'start' | 'end';
}
function Button({ icon, iconPosition = 'start', children }: ButtonProps) {
const direction = useDirection();
const isRTL = direction === 'rtl';
// Flip iconPosition in RTL
const effectivePosition = isRTL
? (iconPosition === 'start' ? 'end' : 'start')
: iconPosition;
return (
<button>
{effectivePosition === 'start' && icon}
{children}
{effectivePosition === 'end' && icon}
</button>
);
}Pattern: CSS Variables for Direction
:root {
--direction: 1;
--reverse-direction: -1;
}
[dir="rtl"] {
--direction: -1;
--reverse-direction: 1;
}
.icon-arrow {
transform: scaleX(var(--direction));
}Documentation
Document RTL support in your design system:
10. Performance Considerations
CSS vs JavaScript
Prefer CSS solutions over JavaScript for RTL:
[dir="rtl"] selectors: Browser-optimizedMinimize Re-renders
If you must use JavaScript for RTL logic:
// ❌ Bad: Recalculates on every render
function Component() {
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
const style = { [isRTL ? 'right' : 'left']: 0 };
return <div style={style}>...</div>;
}
// ✅ Good: Memoize direction check
function Component() {
const direction = useDirection(); // Memoized hook
const style = useMemo(() => ({
[direction === 'rtl' ? 'right' : 'left']: 0
}), [direction]);
return <div style={style}>...</div>;
}
// ✅✅ Best: Use CSS logical properties (no JS needed)
function Component() {
return <div style={{ insetInlineStart: 0 }}>...</div>;
}Bundle Size
Recommendation: Use CSS logical properties to avoid JavaScript overhead.
11. Migration Strategy
Step-by-Step Migration
Phase 1: Audit Current Code
1. Search for physical properties: margin-left, padding-right, etc.
2. Identify components that need RTL support
3. List icons that should/shouldn't mirror
4. Document current layout assumptions
Phase 2: Set Up Direction Switching
1. Add direction toggle in development
2. Create RTL test environment
3. Set up visual regression testing
Phase 3: Migrate CSS
1. Replace physical properties with logical properties
2. Update Flexbox/Grid to use logical values
3. Test each component as you migrate
Phase 4: Update Components
1. Add dir attribute support
2. Update component logic for RTL
3. Handle icon mirroring
4. Test interactions
Phase 5: Test and Polish
1. Test with real RTL languages
2. Fix edge cases
3. Update documentation
4. Train team on RTL patterns
Gradual Migration
You don't need to migrate everything at once:
/* Start with new components */
.new-component {
margin-inline-start: 20px; /* RTL-ready */
}
/* Migrate old components gradually */
.old-component {
margin-left: 20px; /* Still works in LTR */
}
[dir="rtl"] .old-component {
margin-left: 0;
margin-right: 20px; /* RTL override */
}Tools for Migration
Key Takeaways
margin-inline-start instead of margin-left) for automatic RTL supportdir='rtl' on HTML element or component root, not just CSS direction propertyunicode-bidi: isolatetext-align: start/end instead of left/right