Overview / Introduction
Supporting multiple languages is common.
Supporting multiple reading directions in the same UI is where things get tricky.
In regions where languages like Dari, Pashto, and Uzbeki are used, the user interface needs to flow right-to-left (RTL) instead of the traditional left-to-right (LTR) direction used by English.
This post explains how a production frontend application implemented RTL support cleanly using Tailwind CSS, without maintaining separate UI codebases or duplicating styles.
Your app works perfectly in English… until you switch to Arabic and suddenly the UI looks broken.
If a language needs to be read in the opposite direction (RTL instead of LTR) for better readability,
would you create a completely new page/layout for that translation?
At first glance, it might feel simpler:
- One layout for LTR languages
- Another layout for RTL languages
But is that really the right approach?
A better way to think about it
From what I’ve learned, RTL support shouldn’t mean duplicating pages.
Instead of creating separate pages, a more scalable approach is to:
-
Let layout direction drive the UI
-
Treat LTR and RTL as layout modes, not different screens
-
Use a single codebase that adapts based on direction
Problem Statement:
The core challenge was supporting Right-to-Left (RTL) languages within an existing Left-to-Right (LTR) frontend architecture without duplicating UI code.
RTL languages affect more than text alignment — they change the entire visual and interaction flow of the interface. Layouts, spacing, navigation patterns, icons, and animations all need to adapt based on reading direction.
The limitation was that the application needed to:
-
Serve both LTR and RTL users from the same frontend
-
Reuse the same components and layouts
-
Avoid maintaining parallel styles or separate pages
Achieving this in a scalable, maintainable way was the primary challenge.
Initial Approaches Tried:
Before arriving at a clean RTL solution, we experimented with a few common approaches that seemed reasonable at first but did not scale well in practice.
- Manual CSS Overrides for RTL
The first approach was to handle RTL by manually overriding styles when an RTL language was selected. This included:
-
Switching text-align values
-
Replacing margin-left with margin-right
-
Writing RTL-specific CSS rules
-
Applying conditional class names
Why this didn’t work:
-
Styles quickly became fragmented and hard to manage
-
Small UI changes required updating multiple override rules
-
Easy to miss edge cases, especially in complex layouts
-
Increased risk of regressions over time
As the UI grew, this approach became brittle and error-prone.
- Maintaining Separate RTL Styles or Layouts
Another approach was to maintain separate styles or layout variations specifically for RTL use cases.
Why this didn’t work:
-
Significant duplication of styles and logic
-
Every new feature required RTL-specific updates
-
Bug fixes had to be applied in multiple places
-
Long-term maintenance cost increased rapidly
This approach did not scale for a growing component-based frontend.
Why a Direction-First Tailwind Strategy Worked ?
The final approach was chosen to support RTL and LTR layouts in a single multilingual UI without duplicating code or introducing complex conditional logic.
Rather than tying layout behavior to specific languages, the solution treats reading direction (ltr / rtl) as the primary driver of UI behavior. This keeps language translation concerns separate from layout logic and makes the system easier to extend.
Using Tailwind CSS RTL utilities/plugins allowed spacing, alignment, and layout flow to adapt automatically based on direction. This eliminated the need for manual CSS overrides, left/right-specific styles, and RTL-only components.
As a result, components could be written once and reused across all languages, with Tailwind handling direction-aware styling consistently and predictably.
How RTL Support Was Implemented Using Tailwind CSS
RTL support was implemented after upgrading to Tailwind CSS v3+, which provides the flexibility needed to support direction-aware styling through RTL utilities/plugins and logical CSS properties.
The solution was designed to be centralized and direction-driven, ensuring consistent RTL and LTR behavior across a multilingual UI without duplicating styles or components
How RTL & LTR Were Implemented
1. Language → Direction Mapping
The active language is stored in global state.
A simple boolean flag (e.g. isRTL) is derived to indicate whether the current language requires RTL layout.
This keeps language logic separate from layout logic.
2. useTranslation as the Direction & Content Bridge
To keep translation and layout logic centralized, we use a custom useTranslation hook.
Instead of hardcoding labels inside components, each component provides a content object (translations), and the hook returns:
translate()→ fetch localized textlanguage→ current languageisRTL/notEnglish→ direction flag
This keeps text + direction in one place, while keeping components clean.
Step 1 — Define Translatable Content
Each component defines its labels in a simple object:
// InputFieldContent.ts
export const inputFieldContent = {
title: {
english: "Phone Number",
dari: "شماره تلفن",
pashto: "د تلیفون شمېره",
uzbeki: "Telefon raqami",
},
verify: {
english: "Verify",
dari: "تایید",
pashto: "تصدیق",
uzbeki: "Tasdiqlash",
},
};
All translations live in one place (easy to maintain).
Step 2 — Pass Content into useTranslation
Inside the component:
import useTranslation from "../../components/CustomHooks/useTranslation";
import { inputFieldContent } from "./InputFieldContent";
const { translate, isRTL } = useTranslation(inputFieldContent);
Here:
translate(key)returns the correct language stringisRTLcontrols layout direction
Step 3 — Use in JSX
Text
<label>{translate("title")}</label>
Direction
<div dir={isRTL ? "rtl" : "ltr"}>
Styling
className={isRTL ? "text-right" : "text-left"}
Step 4 — Inside the Hook (Simplified Logic)
const useTranslation = (content) => {
const language = useSelector((state) => state.language.language);
const isRTL = ["dari", "pashto"].includes(language);
const translate = (key) => content[key]?.[language] || "";
return { translate, language, isRTL };
};
This makes the hook:
- the single source of truth for language
- the bridge between translation and direction
3. Applying Direction Using the dir Attribute
Direction is applied at container boundaries using the dir attribute:
<div dir={isRTL ? "rtl" : "ltr"}>
All child elements inherit the correct reading direction automatically, allowing native browser behavior to handle text flow and alignment.
Some components (e.g. numeric data, phone inputs) intentionally use dir=“ltr” even in RTL contexts.
4. Direction-Aware Styling Patterns
When browser inference is not enough, conditional Tailwind classes are used.
Spacing (Padding & Margin)
className={isRTL ? "pl-3" : "pr-3"}
className={isRTL ? "mr-4" : "ml-4"}
Borders
className={isRTL ? "border-l" : "border-r"}
Text Alignment
className={isRTL ? "text-right" : "text-left"}
Absolute Positioning
className={isRTL ? "left-5" : "right-5"}
These patterns ensure visual symmetry without duplicating components.
5. Component-Level Direction Handling
Reusable components are direction-aware but language-agnostic.
-
Most components inherit direction from parent containers
-
Direction is passed explicitly only when required
-
Components never check language codes directly
This keeps the component system clean and reusable.
6. Handling Third-Party Components
For third-party UI libraries that don’t fully support RTL:
-
Direction-based overrides are applied using [dir=“rtl”] selectors
-
Numeric input fields are forced to remain LTR
-
Visual elements are adjusted to match RTL flow
7. Icons & Interaction Direction
Directional UI elements such as arrows, navigation steps, and progress indicators are flipped in RTL to preserve intuitive interaction flow.
Outcome / Benefits
After implementing direction-aware RTL/LTR support, the frontend saw clear improvements:
Maintainability
-
Single codebase for both RTL and LTR
-
No duplicated components or styles
-
Centralized, predictable direction logic
User Experience (UX)
-
Natural reading and navigation for RTL users
-
Intuitive layout flow and icon behavior
-
Numeric content remains clear and readable
Scalability
-
New languages added without UI refactoring
-
Components adapt automatically to direction
-
RTL scales across the entire component system
Performance & Code Quality
-
No runtime layout overhead
-
Native browser handling via dir
-
Cleaner components with fewer edge-case bugs
Lessons Learned / Pitfalls
-
RTL is more than text alignment
It affects layout flow, spacing, icons, and interactions—not just text direction. -
Avoid hardcoding left and right
These don’t scale well. Prefer direction-aware layouts and logical utilities. -
Don’t couple layout logic to language codes
Always derive a direction (ltr / rtl) and let the UI respond to it. -
Numeric content needs special handling
Phone numbers, OTPs, and amounts should remain LTR even in RTL layouts. -
Third-party components need extra attention
Many libraries are not RTL-ready and require overrides using [dir=“rtl”]. -
Absolute positioning won’t auto-flip
Icons and floating elements often need manual direction handling. -
Test interactions, not just visuals
Cursor movement, focus states, and form navigation reveal most RTL issues.
Future Improvements
-
Adopt Tailwind logical utilities fully
Gradually replace conditional spacing (pl / pr, ml / mr) with logical utilities (ps, pe, ms, me) to reduce JSX complexity. -
Standardize isRTL naming
Replace language-based flags with clearer direction-based naming for better readability and long-term maintainability. -
Improve animation direction awareness
Ensure transitions and slide animations automatically respect RTL and LTR flow. -
Expand RTL test coverage
Add automated tests and visual regression checks specifically for RTL scenarios. -
Abstract direction-aware UI primitives
Create reusable components (inputs, buttons, layouts) that handle direction internally.
If you’ve tackled RTL/LTR support in your projects or have suggestions to improve this approach, feel free to share. I’d love to hear different perspectives and learn from the community.

