Skip to content

RTL Layout — Building Right-to-Left Layouts for Arabic and Hebrew

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about RTL Layout. We cover key concepts, practical examples, and best practices to help you master this topic.

RTL layout reverses the horizontal flow for Arabic, Hebrew, Persian, and Urdu, requiring mirroring of UI components, icons, and reading direction.

What You'll Learn

By the end of this tutorial, you'll understand the CSS techniques for RTL layout, how to use logical properties instead of physical ones, how to handle bidirectional text, and how to test RTL layouts effectively.

Why It Matters

Over 400 million people read right-to-left (Arabic, Hebrew, Persian, Urdu). A left-aligned layout that looks clean in English becomes confusing in Arabic. Icons that point right (like "next" arrows) need to point left. Padding, margins, and floats all need to reverse. Ignoring RTL support excludes a massive audience and creates a poor user experience.

Real-World Use

A global news platform adds Arabic support. The layout mirrors: navigation moves from left to right, text aligns to the right, the "next article" arrow points left instead of right, and the sidebar swaps sides. The same CSS uses logical properties (margin-inline-start rather than margin-left) so the layout works in both directions without overrides.

LTR vs RTL Layout

graph LR
    A[LTR Layout
English, French] --> B[Navigation ← Left] A --> C[Content → Align left] A --> D[Next → Right arrow] A --> E[Sidebar → Right side] A --> F[Progress → Left to right] G[RTL Layout
Arabic, Hebrew] --> H[Navigation → Right] G --> I[Content → Align right] G --> J[Next ← Left arrow] G --> K[Sidebar → Left side] G --> L[Progress → Right to left] style A fill:#4a90d9,color:#fff style G fill:#27ae60,color:#fff

CSS Logical Properties

/* styles/rtl-base.css — Logical properties for RTL support */

/* Instead of physical properties, use logical properties */
.card {
    /* ❌ Physical — breaks in RTL */
    margin-left: 16px;
    padding-right: 12px;
    border-left: 2px solid #4a90d9;
    text-align: left;

    /* ✅ Logical — works in both LTR and RTL */
    margin-inline-start: 16px;
    padding-inline-end: 12px;
    border-inline-start: 2px solid #4a90d9;
    text-align: start;
}

/* Common physical → logical mapping */
.component {
    /* Physical → Logical */
    /* left       → inline-start */
    /* right      → inline-end */
    /* top        → block-start */
    /* bottom     → block-end */

    /* Examples */
    float: inline-start;           /* left in LTR, right in RTL */
    inset-inline-start: 0;        /* left: 0 in LTR, right: 0 in RTL */
    margin-block-start: 20px;     /* margin-top: 20px */
    padding-block-end: 10px;      /* padding-bottom: 10px */
}

/* Grid and Flexbox are direction-aware by default */
.grid-container {
    display: grid;
    grid-template-columns: 1fr 2fr 1fr;
    /* Grid auto-flows LTR or RTL based on dir attribute */
}

.flex-container {
    display: flex;
    gap: 16px;
    /* Flex direction respects dir attribute */
}

Setting Direction Dynamically

<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
    <meta charset="UTF-8">
    <title>RTL Dynamic Layout</title>
    <link rel="stylesheet" href="styles/rtl-base.css">
    <link rel="stylesheet" href="styles/rtl-overrides.css">
</head>
<body>
    <header>
        <nav>
            <a href="/">Home</a>
            <a href="/about">About</a>
            <a href="/contact">Contact</a>
        </nav>

        <!-- Locale switcher that updates dir -->
        <select id="locale-switcher" aria-label="Select language">
            <option value="en" dir="ltr">English</option>
            <option value="ar" dir="rtl">العربية</option>
            <option value="he" dir="rtl">עברית</option>
        </select>
    </header>

    <main>
        <article>
            <h1>Article Title</h1>
            <p>Content with left-aligned text in LTR, right-aligned in RTL.</p>
            <p class="note">Text-align: start respects the document direction.</p>
        </article>
    </main>

    <script>
        // Update direction when locale changes
        document.getElementById('locale-switcher').addEventListener('change', function() {
            const locale = this.value;
            const dir = this.options[this.selectedIndex].getAttribute('dir');

            document.documentElement.lang = locale;
            document.documentElement.dir = dir;

            // Persist preference
            localStorage.setItem('locale', locale);
            localStorage.setItem('dir', dir);

            console.log(`Switched to ${locale} (${dir})`);
        });

        // Restore saved preference on load
        const savedDir = localStorage.getItem('dir');
        if (savedDir) {
            document.documentElement.dir = savedDir;
        }
    </script>
</body>
</html>

Icon Mirroring

/* styles/rtl-icons.css — Icon mirroring for RTL */

/* Icons that indicate direction need to flip in RTL */
[dir="rtl"] .icon-arrow-right {
    transform: scaleX(-1);
}

[dir="rtl"] .icon-chevron-left {
    transform: scaleX(-1);
}

[dir="rtl"] .icon-caret-down {
    /* Down arrows don't need flipping — they're vertical */
    transform: none;
}

/* Using logical properties for icon positioning */
.icon-next {
    margin-inline-start: 8px;
    /* In LTR: margin-left: 8px (icon is right of text) */
    /* In RTL: margin-right: 8px (icon is left of text) */
}

/* SVG icons with direction-aware transforms */
[dir="rtl"] .icon-arrow svg {
    transform: scaleX(-1);
}

/* Non-directional icons should NOT flip */
[dir="rtl"] .icon-search svg,
[dir="rtl"] .icon-settings svg,
[dir="rtl"] .icon-user svg {
    transform: none;
}

JavaScript for Direction Detection

// i18n/direction.js — Direction utilities
class DirectionManager {
    constructor() {
        this.rtlLocales = ['ar', 'arc', 'ckb', 'dv', 'fa', 'ha', 'he',
            'khw', 'ks', 'ku', 'ps', 'sd', 'ur', 'yi'];
    }

    // Get direction for a locale
    getDirection(locale) {
        const lang = locale.split('-')[0].toLowerCase();
        return this.rtlLocales.includes(lang) ? 'rtl' : 'ltr';
    }

    // Check if current direction is RTL
    isRTL() {
        return document.documentElement.dir === 'rtl'
            || this.getDirection(navigator.language) === 'rtl';
    }

    // Set direction on document
    setDirection(locale) {
        const dir = this.getDirection(locale);
        document.documentElement.dir = dir;
        document.documentElement.lang = locale;
        return dir;
    }

    // Get directional value
    getValue(ltr, rtl) {
        return this.isRTL() ? rtl : ltr;
    }

    // Start (left in LTR, right in RTL)
    get start() {
        return this.isRTL() ? 'right' : 'left';
    }

    // End (right in LTR, left in RTL)
    get end() {
        return this.isRTL() ? 'left' : 'right';
    }
}

const direction = new DirectionManager();

// Usage in JavaScript animations/positioning
function positionTooltip(element) {
    const x = direction.isRTL()
        ? element.getBoundingClientRect().left - 10
        : element.getBoundingClientRect().right + 10;

    console.log(`Tooltip positioned at ${x}px from ${direction.start}`);
}

Testing RTL Layout

// i18n/rtl-testing.js — Automated RTL testing utilities
class RTLTester {
    constructor() {
        this.issues = [];
    }

    // Check for hardcoded physical properties in CSS
    checkCSS(cssText) {
        const physicalProps = [
            /margin-left/g, /margin-right/g,
            /padding-left/g, /padding-right/g,
            /left:\s*\d/g, /right:\s*\d/g,
            /border-left/g, /border-right/g,
            /text-align:\s*(left|right)/g,
            /float:\s*(left|right)/g
        ];

        physicalProps.forEach(pattern => {
            const matches = cssText.match(pattern);
            if (matches) {
                this.issues.push({
                    type: 'Physical CSS property',
                    pattern: pattern.source,
                    count: matches.length,
                    fix: 'Use logical properties (inline-start/inline-end)'
                });
            }
        });
    }

    // Check for directional assumptions in JavaScript
    checkJS(jsCode) {
        const directionalPatterns = [
            /\.left/g, /\.right/g,
            /'left'/g, /'right'/g,
            /clientX/g,
            /pageX/g
        ];

        directionalPatterns.forEach(pattern => {
            const matches = jsCode.match(pattern);
            if (matches) {
                this.issues.push({
                    type: 'Directional assumption in JS',
                    pattern: pattern.source,
                    count: matches.length,
                    fix: 'Consider direction-aware alternatives'
                });
            }
        });
    }

    // Check for hardcoded icon directions
    checkIcons() {
        document.querySelectorAll('[class*="arrow"], [class*="chevron"], [class*="next"], [class*="prev"]').forEach(el => {
            if (!el.closest('[dir="rtl"]') || !el.style.transform?.includes('scaleX(-1)')) {
                this.issues.push({
                    type: 'Unflipped directional icon',
                    element: el.tagName + (el.className ? '.' + el.className : ''),
                    fix: 'Add [dir="rtl"] .icon-class { transform: scaleX(-1); }'
                });
            }
        });
    }

    // Generate report
    report() {
        if (this.issues.length === 0) {
            return { status: 'PASS', message: 'No RTL issues found' };
        }

        return {
            status: 'ISSUES_FOUND',
            count: this.issues.length,
            issues: this.issues
        };
    }
}

// Usage
const tester = new RTLTester();
// tester.checkCSS(myStylesheet);
// tester.checkJS(myScript);
// tester.checkIcons();
// console.log(tester.report());

Common Mistakes

  1. Using physical CSS properties. margin-left, padding-right, text-align: left all break in RTL. Use logical properties: margin-inline-start, padding-inline-end, text-align: start.
  2. Not mirroring directional icons. A "Next" arrow pointing right should point left in RTL. Use transform: scaleX(-1) with [dir="rtl"] selector for directional icons.
  3. Hardcoding sidebar positions. A sidebar on the right in LTR should be on the left in RTL. Use Flexbox order or grid with auto-flow instead of fixed left/right positioning.
  4. Ignoring bidirectional text. A page in Arabic may contain English product names or URLs. Use the element for bidirectional isolation to prevent the browser from incorrectly reordering mixed-direction text.
  5. Not testing with real RTL content. A layout that looks fine with lorem ipsum in Arabic may break with real text. Use actual Arabic sentences of varying lengths to test wrapping, overflow, and alignment.

Practice Questions

  1. What is the difference between physical and logical CSS properties?
  2. How do you mirror icons in RTL layouts?
  3. What HTML attribute controls text direction?
  4. Why is the element important for bidirectional text?
  5. How do flexbox and grid behave differently in RTL vs LTR?

Challenge: Take an existing LTR layout (a blog page with sidebar, navigation, article cards, and pagination) and convert it to support RTL using only logical properties, no [dir="rtl"] overrides. Add icon mirroring for directional arrows, test with Arabic content, and verify that the layout works correctly in both directions.

FAQ

Do I need to maintain separate CSS files for LTR and RTL?

No. Use logical properties and the dir attribute. The same CSS works for both directions when you use inline-start/inline-end instead of left/right. No separate files or overrides needed.

How does text-align: start work?

text-align: start aligns to the left in LTR and to the right in RTL. Similarly, end aligns to the right in LTR and left in RTL. This is the most RTL-friendly text alignment value.

Do I need to flip every icon?

Only icons that indicate direction: arrows, chevrons, pagination controls. Icons like search, settings, user, home do not need flipping. Test each icon visually in RTL mode.

What is the tag?

(Bidirectional Isolation) isolates a span of text from the surrounding direction. Use it for user-generated content that may be in a different direction: Arabic usernames in an English sentence, or English product names in Arabic text.

{{< faq "How do I handle form inputs in RTL?" "Input text should align based on the content language, not the page language. Use the dir="auto" attribute on inputs so the browser automatically detects direction from the user's typed content." >}}

Mini Project

Build a bilingual blog layout (English + Arabic) that: switches direction based on locale using the dir attribute, uses logical CSS properties exclusively (no left/right), mirrors directional icons, positions sidebar based on reading direction, handles mixed-direction content with , and includes a locale switcher that updates the layout without page reload.

What's Next

You've mastered RTL layout. Next, learn about CSS for RTL — advanced CSS techniques for bidirectional text support.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro