Supporting RTL (Right-to-Left) Languages

Easy

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:

Arabic (العربية)
Hebrew (עברית)
Persian/Farsi (فارسی)
Urdu (اردو)

Unicode Bidirectional Algorithm

Unicode automatically handles text direction using the Bidirectional Algorithm (Bidi):

Strong LTR: English, numbers (0-9)
Strong RTL: Arabic, Hebrew characters
Weak/Neutral: Punctuation, spaces

The algorithm determines direction based on character properties, not just language.

Example: Mixed Content

Hello مرحبا World

The 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;
}
PhysicalLogical (Block)Logical (Inline)
widthblock-sizeinline-size
heightblock-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
leftinset-block-startinset-inline-start
rightinset-block-endinset-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 ✅

Navigation menus (left ↔ right)
Sidebars (left ↔ right)
Icons with direction (arrows, chevrons)
Text alignment
Margins and padding
Positioning (left/right)

Should NOT Mirror ❌

Logos (brand identity)
Images (photos, illustrations)
Icons without direction (heart, star, settings)
Charts and graphs (data visualization)
Video players (controls may mirror, but video doesn't)
Maps (geographic orientation)

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:

LRM (\u200E): Left-to-Right Mark
RLM (\u200F): Right-to-Left Mark
LRE (\u202A): Left-to-Right Embedding
RLE (\u202B): Right-to-Left Embedding
PDF (\u202C): Pop Directional Formatting

Example: 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 LTR

CSS 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

[ ] Set dir="rtl" on HTML element
[ ] Verify text flows right-to-left
[ ] Check layout mirrors correctly
[ ] Test navigation menus
[ ] Verify icons flip appropriately
[ ] Test forms and inputs
[ ] Check modals and dropdowns
[ ] Verify tooltips and popovers
[ ] Test with real RTL text (Arabic/Hebrew)
[ ] Check mixed LTR/RTL content
[ ] Verify numbers display correctly
[ ] Test on mobile devices

Browser 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 featureprefers-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) => (
      &lt;div dir="rtl" lang="ar"&gt;
        &lt;Story /&gt;
      &lt;/div&gt;
    ),
  ],
};

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:

Which components support RTL
Which icons should/shouldn't mirror
How to test RTL layouts
Common patterns and examples

10. Performance Considerations

CSS vs JavaScript

Prefer CSS solutions over JavaScript for RTL:

✅ CSS logical properties: Zero runtime cost
✅ CSS [dir="rtl"] selectors: Browser-optimized
❌ JavaScript direction checks: Runtime overhead

Minimize 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

CSS logical properties: No additional bundle size
RTL libraries: Add ~2-5KB (usually unnecessary)
Custom RTL logic: Depends on implementation

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

PostCSS RTL: Automatically converts LTR CSS to RTL
RTLCSS: CSS processor for RTL conversion
Stylelint: Lint rules to enforce logical properties

Key Takeaways

1Use CSS logical properties (margin-inline-start instead of margin-left) for automatic RTL support
2Set dir='rtl' on HTML element or component root, not just CSS direction property
3Only mirror directional elements (arrows, chevrons), not logos or images
4Test with real RTL text (Arabic/Hebrew), not just flipped English
5Handle mixed LTR/RTL content using Unicode bidi marks or CSS unicode-bidi: isolate
6Prefer CSS solutions over JavaScript for better performance
7Use text-align: start/end instead of left/right
8Design systems should use direction-agnostic tokens from the start
9Migration can be gradual—start with new components, migrate old ones over time
10Always test interactions (dropdowns, modals, tooltips) in RTL mode