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 Layout
// 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
dirattribute 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.