EthicsComputerCourses
Join training +91 90532 10052

RTL Layouts: Urdu & Arabic

Bootstrap · Lesson 36 of 40 Advanced 8 min read Updated

Urdu, Arabic, Persian and Hebrew are written from right to left (RTL). Urdu is one of the 22 languages in the Eighth Schedule of India's Constitution, and many Indian agencies also build sites for clients in the Gulf, so RTL support is a real skill, not a curiosity. In an RTL page, the whole layout is mirrored: the logo moves to the right, menus open towards the left, and the "next" arrow points left.

Bootstrap 5 was designed for this. In this lesson you will switch a page between English and Urdu, learn which classes and CSS work in both directions, generate RTL versions of your own CSS with RTLCSS, and handle the tricky parts: numbers, phone numbers and icons inside RTL text. Bootstrap's documentation still labels its RTL support as experimental, so test carefully.

The three requirements

An RTL Bootstrap page needs three things:

  1. dir="rtl" on the <html> element. This tells the browser the base direction of the text and the layout.
  2. A matching lang attribute, such as lang="ur" for Urdu or lang="ar" for Arabic. Screen readers use it to pick the right voice, and browsers use it for fonts and hyphenation.
  3. Bootstrap's RTL stylesheet, bootstrap.rtl.min.css, instead of the normal one. Never load both.
HTML
<!DOCTYPE html>
<html lang="ur" dir="rtl">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>اردو کتاب گھر</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.rtl.min.css" rel="stylesheet">
</head>

The JavaScript bundle is the same file for both directions: components such as dropdowns and the carousel read the page direction themselves.

One markup, both directions

The secret is that Bootstrap 5 class names never say "left" or "right". They say start and end, which mean left and right in English but right and left in Urdu. Write your markup with these classes and the same HTML works in both directions:

UseInstead of
ms-*, me-*, ps-*, pe-*margin or padding left/right
text-start, text-endtext-align: left/right
float-start, float-endfloats left/right
start-0, end-0 (with position-absolute)left: 0, right: 0
border-start, rounded-endone-sided borders and corners
dropdown-menu-end, dropstart, dropendmenus aligned or opening left/right

Run this page and press the language button. The script swaps the stylesheet, dir, lang and the texts; the HTML structure never changes.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>LTR and RTL</title>
  <link id="bsCss" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
  <style>
    [dir="rtl"] .flip-rtl { display: inline-block; transform: scaleX(-1); }
  </style>
</head>
<body>
  <nav class="navbar navbar-expand bg-body-tertiary border-bottom">
    <div class="container">
      <span class="navbar-brand" data-t="brand">Urdu Book House</span>
      <div class="navbar-nav ms-auto">
        <a class="nav-link" href="#books" data-t="books">Books</a>
        <a class="nav-link" href="#contact" data-t="contact">Contact</a>
      </div>
      <button type="button" class="btn btn-sm btn-primary ms-3" id="lang">اردو</button>
    </div>
  </nav>
  <main class="container py-4">
    <nav aria-label="breadcrumb">
      <ol class="breadcrumb">
        <li class="breadcrumb-item"><a href="#books" data-t="books">Books</a></li>
        <li class="breadcrumb-item active" aria-current="page" data-t="poetry">Poetry</li>
      </ol>
    </nav>
    <div class="card position-relative" style="max-width: 420px">
      <span class="badge text-bg-danger position-absolute top-0 end-0 m-2" data-t="new">New</span>
      <div class="card-body">
        <h1 class="h5 card-title" data-t="title">Diwan-e-Ghalib</h1>
        <p class="mb-2"><span data-t="price">Price</span>: <bdi>₹499</bdi></p>
        <p class="mb-3"><span data-t="phone">Phone</span>: <span dir="ltr">+91 522 400 1234</span></p>
        <div class="input-group mb-3">
          <span class="input-group-text">@</span>
          <input class="form-control" aria-label="Email" placeholder="email">
        </div>
        <a href="#next" class="btn btn-outline-primary"><span data-t="next">Next</span> <span class="flip-rtl" aria-hidden="true">&rarr;</span></a>
      </div>
    </div>
  </main>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
  <script>
    const words = {
      en: { brand: "Urdu Book House", books: "Books", contact: "Contact", poetry: "Poetry", new: "New",
            title: "Diwan-e-Ghalib", price: "Price", phone: "Phone", next: "Next" },
      ur: { brand: "اردو کتاب گھر", books: "کتابیں", contact: "رابطہ", poetry: "شاعری", new: "نیا",
            title: "دیوانِ غالب", price: "قیمت", phone: "فون", next: "اگلا" },
    };
    const css = {
      en: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css",
      ur: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.rtl.min.css",
    };
    document.getElementById("lang").addEventListener("click", (event) => {
      const next = document.documentElement.lang === "en" ? "ur" : "en";
      document.documentElement.lang = next;
      document.documentElement.dir = next === "ur" ? "rtl" : "ltr";
      document.getElementById("bsCss").href = css[next];
      document.querySelectorAll("[data-t]").forEach((el) => { el.textContent = words[next][el.dataset.t]; });
      event.currentTarget.textContent = next === "ur" ? "English" : "اردو";
    });
  </script>
</body>
</html>

In Urdu, the brand moves right, the links move left (ms-auto now pushes from the right), the "New" badge jumps to the other corner (end-0), the breadcrumb reads from right to left, the @ sits on the right of the input, and the arrow points left. Swapping the stylesheet at runtime is fine for a demo; a real site usually serves each language at its own URL with the correct stylesheet in the HTML.

Your own CSS: logical properties and RTLCSS

Bootstrap's RTL file is generated from its normal CSS by a tool called RTLCSS, which swaps left and right values. Your own stylesheet is not touched by that, so you have two options.

Option 1: write logical CSS. Logical properties describe sides by direction of writing instead of by screen position (see Logical Properties). One stylesheet then works in both directions with no build step:

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Logical properties</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
  <style>
    .offer {
      position: relative;
      padding-inline-start: 1rem;              /* padding-left in LTR */
      border-inline-start: 4px solid #f77f00;  /* left border in LTR, right border in RTL */
      text-align: start;
    }
    .offer-tag {
      position: absolute;
      inset-block-start: 0;                    /* top */
      inset-inline-end: 0;                     /* right in LTR, left in RTL */
    }
  </style>
</head>
<body>
  <div class="container py-4">
    <div class="row g-4">
      <div class="col-sm-6">
        <div class="offer bg-body-tertiary rounded p-3" dir="ltr" lang="en">
          <span class="offer-tag badge text-bg-warning">10% off</span>
          <p class="mb-0 mt-4">Festival offer on all poetry books.</p>
        </div>
      </div>
      <div class="col-sm-6">
        <div class="offer bg-body-tertiary rounded p-3" dir="rtl" lang="ur">
          <span class="offer-tag badge text-bg-warning">10% off</span>
          <p class="mb-0 mt-4">شاعری کی تمام کتابوں پر تہوار کی پیشکش۔</p>
        </div>
      </div>
    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

The same CSS puts the orange stripe and the tag on opposite sides in the two boxes, because dir works on any element, not only <html>.

Option 2: run RTLCSS on your compiled CSS. If you have existing CSS full of left and right, install the rtlcss npm package and generate a mirrored copy, just like Bootstrap does:

Terminal
npm install --save-dev rtlcss
npx rtlcss css/site.css css/site.rtl.css

Given this input, RTLCSS 4.3 writes the output shown below it. A special comment, /* rtl:ignore */, tells it to leave a rule alone, which you want for things that must stay left-to-right, such as phone numbers:

CSS
.promo-ribbon { right: 0; margin-left: 1rem; border-left: 4px solid #f77f00; text-align: left; }
/* rtl:ignore */
.phone-number { direction: ltr; text-align: left; }
.next-arrow { transform: translateX(4px); }
Output
.promo-ribbon { left: 0; margin-right: 1rem; border-right: 4px solid #f77f00; text-align: right; }
.phone-number { direction: ltr; text-align: left; }
.next-arrow { transform: translateX(-4px); }

RTLCSS also understands value directives that change only one value in the RTL copy. Urdu and Arabic letters join to each other, so the wide letter spacing you might give an English heading should be removed in RTL. In Sass, keep the comment in the output with interpolation:

SCSS
$brand-spacing: .08em #{"/* rtl:0 */"};
.brand-title { letter-spacing: $brand-spacing; }

The LTR file keeps .08em; after RTLCSS, the RTL file has letter-spacing: 0.

Mixed text: numbers, Latin words and icons

Real Urdu pages contain English product names, prices, phone numbers, email addresses and code. The browser's bidirectional algorithm usually handles single numbers and words, but sequences with spaces, plus signs or punctuation can come out scrambled. Three tools fix this:

  • <span dir="ltr"> for content that must always read left to right, such as phone numbers (+91 522 400 1234), email addresses and code.
  • <bdi> (bidirectional isolate) around values whose direction you do not control, such as a user's name or a price inserted by your server.
  • dir="auto" on inputs and text areas where users may type either English or Urdu, so each message takes the direction of its first strong character.

Icons need thought too. Arrows and chevrons that mean "forward" or "back" should flip, as the flip-rtl class above does. Icons that show real-world objects, such as a clock, a play button or a tick, should not flip.

Both directions in one stylesheet

Most sites serve separate LTR and RTL files. If one page must switch without reloading styles, Bootstrap's docs describe two approaches: wrapping Bootstrap's import in .ltr and .rtl classes with an RTLCSS string map, or the PostCSS plugin postcss-rtlcss, which writes both versions into one file with [dir] prefixes. The docs warn that the combined file is about 20 to 30 percent larger, so use it only when you truly need runtime switching.

Common mistakes

  • Setting dir="rtl" but loading bootstrap.min.css. Text aligns right but margins, menus and corners stay on the LTR side. Load bootstrap.rtl.min.css.
  • Forgetting lang. Screen readers read Urdu text with an English voice. Always pair dir with lang.
  • Using left/right in your own CSS. It does not flip. Use logical properties or run RTLCSS.
  • Flipping every icon. Only direction-meaning icons such as arrows should mirror.
  • Phone numbers and emails without dir="ltr". The plus sign and groups of digits can end up in the wrong order.

Key points

  • RTL pages need dir="rtl", a matching lang and bootstrap.rtl.min.css instead of the normal file; the JavaScript bundle is the same.
  • Bootstrap's start/end classes (ms-*, text-end, end-0, dropdown-menu-end) let one markup serve both directions.
  • For your own CSS, use logical properties, or generate a mirrored copy with RTLCSS; /* rtl:ignore */ and value directives control exceptions.
  • Wrap always-LTR content in dir="ltr", unknown-direction values in <bdi>, and use dir="auto" on user inputs.
  • Mirror direction arrows, not real-world icons, and test every page in both directions.

Exercise

Make an Urdu version of a small "contact us" card for a Lucknow bakery. It needs lang="ur" and dir="rtl", the RTL stylesheet, a heading, a phone number that always reads left to right, an email input with a @ add-on in an input group, and a "Send" button whose arrow points in the reading direction. Use only Bootstrap classes and one small CSS rule for the arrow.

Show solution
HTML
<!DOCTYPE html>
<html lang="ur" dir="rtl">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>رابطہ</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.rtl.min.css" rel="stylesheet">
  <style>[dir="rtl"] .flip-rtl { display: inline-block; transform: scaleX(-1); }</style>
</head>
<body>
  <main class="container py-4" style="max-width: 480px">
    <div class="card"><div class="card-body">
      <h1 class="h5">رابطہ</h1>
      <p>فون: <span dir="ltr">+91 522 400 5678</span></p>
      <div class="input-group mb-3">
        <span class="input-group-text">@</span>
        <input type="email" class="form-control" aria-label="ای میل" dir="ltr">
      </div>
      <button type="button" class="btn btn-primary">بھیجیں <span class="flip-rtl" aria-hidden="true">&rarr;</span></button>
    </div></div>
  </main>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

The RTL stylesheet mirrors the input group and spacing, dir="ltr" keeps the phone number and the email address in their natural order, and the arrow is flipped only in RTL.

Quick Quiz

1. Which three things does a Bootstrap RTL page need?

2. Why does ms-auto work in both English and Urdu layouts?

3. What does the comment /* rtl:ignore */ above a rule do?

4. A phone number +91 522 400 1234 appears scrambled in Urdu text. What is the fix?

Learn Bootstrap with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call