Front-end Engineering Lab
PatternsInternationalization

RTL Support

Implementing right-to-left language support for Arabic, Hebrew, and Persian

RTL Support (Right-to-Left)

RTL (Right-to-Left) support is essential for languages like Arabic, Hebrew, Persian, and Urdu. It's not just about flipping text direction—it affects layout, icons, animations, and user interactions.

RTL Languages

Major RTL Languages:

  • Arabic (ar): 420M+ speakers
  • Hebrew (he): 9M+ speakers
  • Persian/Farsi (fa): 110M+ speakers
  • Urdu (ur): 230M+ speakers

The Challenge

RTL is not just flipping text:

  • Layout direction changes
  • Icons need mirroring
  • Animations flow differently
  • Scrolling direction inverts
  • Text alignment shifts

Basic RTL Setup

HTML Direction

// app/[locale]/layout.tsx
import { getDirection } from '@/lib/i18n';

export default function RootLayout({ 
  children,
  params: { locale }
}: Props) {
  const direction = getDirection(locale);
  
  return (
    <html lang={locale} dir={direction}>
      <body>{children}</body>
    </html>
  );
}

// lib/i18n.ts
export function getDirection(locale: string): 'ltr' | 'rtl' {
  const rtlLocales = ['ar', 'he', 'fa', 'ur'];
  return rtlLocales.includes(locale) ? 'rtl' : 'ltr';
}

CSS Direction

/* Automatic with [dir] attribute */
html[dir='rtl'] {
  direction: rtl;
}

html[dir='ltr'] {
  direction: ltr;
}

Tailwind CSS RTL Support

Setup

npm install tailwindcss-rtl
// tailwind.config.js
module.exports = {
  plugins: [
    require('tailwindcss-rtl'),
  ],
};

Usage

// Auto-flips based on direction
<div className="ml-4 rtl:mr-4 rtl:ml-0">
  Content with proper margins
</div>

// Left/Right become Start/End
<div className="text-left rtl:text-right">
  Text aligned to start
</div>

// Padding
<div className="pl-4 rtl:pr-4 rtl:pl-0">
  Padding on start side
</div>

// Border
<div className="border-l-2 rtl:border-r-2 rtl:border-l-0">
  Border on start side
</div>

Logical Properties (Better Approach)

/* Instead of left/right, use start/end */
.element {
  /* ❌ Direction-specific */
  margin-left: 1rem;
  padding-right: 2rem;
  
  /* ✅ Direction-agnostic */
  margin-inline-start: 1rem;
  padding-inline-end: 2rem;
}
// Tailwind with logical properties
<div className="ms-4 pe-8">
  {/* ms = margin-inline-start, pe = padding-inline-end */}
  Content with logical spacing
</div>

Component Patterns

Flex Direction

// ❌ BAD: Fixed direction
<div className="flex flex-row">
  <Icon />
  <span>Text</span>
</div>

// ✅ GOOD: Responsive direction
<div className="flex flex-row rtl:flex-row-reverse">
  <Icon />
  <span>Text</span>
</div>

// ✅ BETTER: Use gap instead of margins
<div className="flex flex-row rtl:flex-row-reverse gap-2">
  <Icon />
  <span>Text</span>
</div>

Icons That Need Flipping

// Icons that indicate direction should flip
function NavigationIcon({ direction }: { direction: 'next' | 'prev' }) {
  const isRtl = useIsRTL();
  
  return (
    <ArrowRightIcon 
      className={`
        ${direction === 'prev' && 'rotate-180'}
        ${isRtl && 'scale-x-[-1]'}
      `}
    />
  );
}

// Simpler with custom hook
function useFlipIcon(shouldFlip: boolean) {
  const isRtl = useIsRTL();
  return shouldFlip && isRtl ? 'scale-x-[-1]' : '';
}

Icons That Don't Flip

// These should NOT flip:
// - Play/Pause icons
// - Volume icons
// - Checkmarks
// - Close (X) icons
// - Plus/Minus icons

<PlayIcon className="no-flip" />
<CheckIcon className="no-flip" />

Text Alignment

// ❌ BAD: Hardcoded alignment
<p className="text-left">
  This text is always left-aligned
</p>

// ✅ GOOD: Responsive alignment
<p className="text-left rtl:text-right">
  This text aligns to the start
</p>

// ✅ BETTER: Use start/end
<p className="text-start">
  This text aligns to the start automatically
</p>

Forms and Inputs

// Input with icon
function SearchInput() {
  return (
    <div className="relative">
      <input 
        type="search" 
        className="
          w-full
          pl-10 rtl:pr-10 rtl:pl-4
          pr-4 rtl:pl-10 rtl:pr-4
        "
      />
      <SearchIcon className="
        absolute 
        left-3 rtl:right-3 rtl:left-auto
        top-1/2 
        -translate-y-1/2
      " />
    </div>
  );
}

// Better with logical properties
function SearchInput() {
  return (
    <div className="relative">
      <input 
        type="search" 
        className="w-full ps-10 pe-4"
      />
      <SearchIcon className="
        absolute 
        start-3
        top-1/2 
        -translate-y-1/2
      " />
    </div>
  );
}

Animations and Transitions

/* Slide from right in LTR, from left in RTL */
@keyframes slideIn {
  from {
    transform: translateX(100%);
  }
  to {
    transform: translateX(0);
  }
}

[dir='rtl'] .slide-in {
  animation: slideInRtl 0.3s;
}

[dir='ltr'] .slide-in {
  animation: slideIn 0.3s;
}

@keyframes slideInRtl {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(0);
  }
}

Scrolling

// Horizontal scroll direction
function HorizontalScroll() {
  const scrollRef = useRef<HTMLDivElement>(null);
  const isRtl = useIsRTL();
  
  const scroll = (direction: 'left' | 'right') => {
    const el = scrollRef.current;
    if (!el) return;
    
    const scrollAmount = 200;
    const multiplier = direction === 'right' ? 1 : -1;
    const rtlMultiplier = isRtl ? -1 : 1;
    
    el.scrollBy({
      left: scrollAmount * multiplier * rtlMultiplier,
      behavior: 'smooth',
    });
  };
  
  return (
    <div>
      <button onClick={() => scroll('left')}>←</button>
      <div ref={scrollRef} className="overflow-x-auto">
        {/* Scrollable content */}
      </div>
      <button onClick={() => scroll('right')}>→</button>
    </div>
  );
}

Complex Layouts

Grid Layout

// ❌ BAD: Fixed grid flow
<div className="grid grid-cols-3">
  <div>Column 1</div>
  <div>Column 2</div>
  <div>Column 3</div>
</div>

// ✅ GOOD: RTL-aware grid
<div className="grid grid-cols-3 [grid-auto-flow:dense]">
  <div className="col-start-1 rtl:col-start-3">Column 1</div>
  <div className="col-start-2">Column 2</div>
  <div className="col-start-3 rtl:col-start-1">Column 3</div>
</div>
// Sidebar that switches sides
function Layout() {
  return (
    <div className="flex">
      <aside className="order-1 rtl:order-2">
        Sidebar
      </aside>
      <main className="order-2 rtl:order-1 flex-1">
        Main Content
      </main>
    </div>
  );
}

Utility Functions

// hooks/useIsRTL.ts
import { useLocale } from 'next-intl';

export function useIsRTL(): boolean {
  const locale = useLocale();
  const rtlLocales = ['ar', 'he', 'fa', 'ur'];
  return rtlLocales.includes(locale);
}

// hooks/useDirection.ts
export function useDirection(): 'ltr' | 'rtl' {
  const isRtl = useIsRTL();
  return isRtl ? 'rtl' : 'ltr';
}

// utils/rtl.ts
export function getStartPosition(isRtl: boolean, ltrValue: number, rtlValue?: number) {
  return isRtl ? (rtlValue ?? ltrValue) : ltrValue;
}

export function getEndPosition(isRtl: boolean, ltrValue: number, rtlValue?: number) {
  return isRtl ? ltrValue : (rtlValue ?? ltrValue);
}

Testing RTL

// __tests__/rtl.test.tsx
import { render } from '@testing-library/react';
import { IntlProvider } from 'next-intl';

function renderWithLocale(component: React.ReactElement, locale: string) {
  return render(
    <IntlProvider locale={locale} messages={{}}>
      <html lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}>
        <body>{component}</body>
      </html>
    </IntlProvider>
  );
}

test('button aligns correctly in RTL', () => {
  const { container } = renderWithLocale(<Button />, 'ar');
  
  const button = container.querySelector('button');
  expect(button).toHaveStyle({ textAlign: 'right' });
});

Browser DevTools

// Toggle RTL in Chrome DevTools Console
document.dir = 'rtl';

// Or add to body
document.body.style.direction = 'rtl';

Common Pitfalls

Using left/right instead of logical properties
Use start/end, inline-start/inline-end

Forgetting to flip interactive elements
Test all interactions in RTL

Flipping all icons
Only flip directional icons

Not testing with real Arabic text
Test with actual RTL content

Assuming RTL is just CSS
Consider layout, animations, interactions

RTL Checklist

  • Set dir attribute on HTML element
  • Use logical CSS properties (start/end)
  • Test all layouts in RTL mode
  • Flip directional icons
  • Check form inputs and labels
  • Test horizontal scrolling
  • Verify animations flow correctly
  • Check tooltips and popovers
  • Test dropdown menus
  • Verify charts and graphs
  • Check calendar components
  • Test image galleries
  • Verify navigation flows

Tools and Resources

Browser Extensions:

  • RTL Tester (Chrome)
  • RTL Switcher (Firefox)

Online Tools:

  • rtlcss.com: Convert LTR CSS to RTL
  • arabic.design: Arabic typography

Testing:

  • Test with native Arabic speakers
  • Use real Arabic content (not Lorem Ipsum)
  • Check on actual Arabic devices

RTL support is not an afterthought—build it in from the start for truly global applications.

On this page