How We Supported Arabic & Afghan Languages Without Duplicating Our UI (Tailwind CSS Approach)

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?

:thinking: 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.

  1. 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.

  1. 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 text
  • language → current language
  • isRTL / 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 string
  • isRTL controls 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.

1 Like