RTL Layouts: Urdu & Arabic
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:
dir="rtl"on the<html>element. This tells the browser the base direction of the text and the layout.- A matching
langattribute, such aslang="ur"for Urdu orlang="ar"for Arabic. Screen readers use it to pick the right voice, and browsers use it for fonts and hyphenation. - Bootstrap's RTL stylesheet,
bootstrap.rtl.min.css, instead of the normal one. Never load both.
<!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:
| Use | Instead of |
|---|---|
ms-*, me-*, ps-*, pe-* | margin or padding left/right |
text-start, text-end | text-align: left/right |
float-start, float-end | floats left/right |
start-0, end-0 (with position-absolute) | left: 0, right: 0 |
border-start, rounded-end | one-sided borders and corners |
dropdown-menu-end, dropstart, dropend | menus 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.
<!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">→</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:
<!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:
npm install --save-dev rtlcss
npx rtlcss css/site.css css/site.rtl.cssGiven 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:
.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); }.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:
$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 loadingbootstrap.min.css. Text aligns right but margins, menus and corners stay on the LTR side. Loadbootstrap.rtl.min.css. - Forgetting
lang. Screen readers read Urdu text with an English voice. Always pairdirwithlang. - Using
left/rightin 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 matchinglangandbootstrap.rtl.min.cssinstead 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 usedir="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
<!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">→</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?
Direction, language and the RTL stylesheet work together; the JS bundle stays the same.
2. Why does ms-auto work in both English and Urdu layouts?
Start and end follow the writing direction, so the class does the right thing in both.
3. What does the comment /* rtl:ignore */ above a rule do?
RTLCSS copies the rule unchanged into the RTL output.
4. A phone number +91 522 400 1234 appears scrambled in Urdu text. What is the fix?
Forcing left-to-right direction keeps the plus sign and digit groups in order.
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
