DEV Community

Building RTL (Persian/Arabic) Websites with Bootstrap 5: A Practical Guide

Getting a Persian or Arabic site to look right takes more than text-align: right. Direction affects layout, spacing, icons, numbers, form fields and even your carousel arrows. Below is the checklist I use for every right-to-left (RTL) project with Bootstrap 5, plus a free starter template you can clone.

TL;DR: set dir="rtl" on <html>, load bootstrap.rtl.min.css, use logical properties in your own CSS, flip only the icons that show direction, and test with real Persian/Arabic text.

1. Start at the root: lang and dir

<!doctype html>
<html lang="fa" dir="rtl">
Enter fullscreen mode Exit fullscreen mode

dir="rtl" changes how the browser lays out the page: inline flow, table column order, scrollbar side in some browsers, and the default alignment of text. lang matters too. It drives hyphenation, font selection, and screen reader pronunciation. Use fa for Persian and ar for Arabic.

Don't set direction with CSS (direction: rtl on body). The HTML attribute is semantic, it's inherited by form controls, and the :dir() selector and Bootstrap both rely on it.

2. Use Bootstrap's official RTL build

Since v5, Bootstrap ships a full RTL stylesheet generated with RTLCSS. Swap one file:

<link rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.rtl.min.css">
Enter fullscreen mode Exit fullscreen mode

What you get for free:

  • ms-* / me-* and ps-* / pe-* mean start and end, so in RTL ms-3 puts the margin on the right.
  • text-start aligns right, float-end floats left, and so on.
  • Navbar, dropdowns, input groups, breadcrumbs, pagination and modals are all mirrored.

A common trap: an icon placed before its label needs space at its end, so it gets me-2 in both LTR and RTL. If you "fix" it to ms-2 because you think in left/right, the icon ends up stuck to the text. Think "start = where reading begins".

Using Sass? Compile your own build and run it through RTLCSS. You can keep one source and produce style.css and style.rtl.css. Bootstrap uses the /* rtl:ignore */ and /* rtl:raw: */ directives for exceptions.

3. Write your own CSS with logical properties

Your custom CSS isn't flipped automatically when you just use the CDN file. Logical properties remove the need for a second stylesheet:

Physical (avoid) Logical (use)
margin-left margin-inline-start
padding-right padding-inline-end
left: 0 inset-inline-start: 0
border-left border-inline-start
text-align: left text-align: start
border-top-left-radius border-start-start-radius
.sidebar {
  position: fixed;
  inset-inline-start: 0;   /* right in RTL, left in LTR */
  border-inline-end: 1px solid var(--bs-border-color);
}
Enter fullscreen mode Exit fullscreen mode

Browser support is universal in evergreen browsers. Transforms are the exception: translateX(-100%) doesn't flip, so off-canvas animations need a direction-aware rule:

.sidebar { transform: translateX(100%); }
[dir="ltr"] .sidebar { transform: translateX(-100%); }
Enter fullscreen mode Exit fullscreen mode

4. Fonts: pick one designed for Persian/Arabic

System fallbacks for Persian vary a lot by OS and often look cramped. Vazirmatn is a free (OFL) variable font with Persian and Arabic glyphs and good Latin support:

<link rel="stylesheet"
  href="https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/Vazirmatn-font-face.css">
Enter fullscreen mode Exit fullscreen mode
:root { --bs-body-font-family: Vazirmatn, system-ui, sans-serif; }
body { line-height: 1.9; }
Enter fullscreen mode Exit fullscreen mode

Two tips:

  • Increase line-height. Arabic-script letters have tall ascenders and dots below the baseline, so 1.5 often feels tight. 1.8–2 reads better for body text.
  • Don't use letter-spacing on Persian/Arabic text. The script is cursive, and spacing breaks the joins between letters.

5. Icons: flip only what shows direction

Not every icon should be mirrored. A rule of thumb:

  • Flip: arrows, chevrons, "back/next", reply, send, undo/redo, list-with-bullets, progress indicators.
  • Don't flip: search, check marks, clocks (clocks run clockwise everywhere), media play buttons (debatable, but most RTL apps keep ▶), logos, and anything that shows a real-world object.

With Bootstrap Icons, just choose the opposite glyph in RTL (bi-chevron-left for "next"), or add a utility:

[dir="rtl"] .flip-rtl { transform: scaleX(-1); }
Enter fullscreen mode Exit fullscreen mode

Bootstrap's carousel controls and breadcrumb separators are already handled by the RTL build.

6. Numbers, forms and mixed content

This is where most RTL bugs live.

Digits. Persian users expect Persian digits (۰۱۲۳…) in content. Convert at render time, not in your data:

const toFa = s => String(s).replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[d]);
document.querySelectorAll('.fa-num').forEach(el => el.textContent = toFa(el.textContent));
Enter fullscreen mode Exit fullscreen mode

For prices, Intl.NumberFormat('fa-IR').format(1250000) gives ۱٬۲۵۰٬۰۰۰ with the correct separator.

Inputs. Fields like email, URL, phone, card numbers and codes should stay LTR even on an RTL page:

<input type="email" class="form-control" dir="ltr">
<input type="tel" class="form-control text-start" dir="ltr" inputmode="numeric">
Enter fullscreen mode Exit fullscreen mode

On the server, normalize what users type. Persian keyboards produce ۱۲۳, Arabic keyboards produce ١٢٣, so convert both to ASCII before validation.

Mixed direction text. A phrase like "Bootstrap 5 RTL" inside a Persian sentence is usually fine. User-generated strings, usernames and anything ending in punctuation can jump around, though. Wrap them:

<bdi>{{ username }}</bdi>
<span dir="auto">{{ user_comment }}</span>
Enter fullscreen mode Exit fullscreen mode

dir="auto" on text inputs and textareas also helps when users type in either language.

7. Tables, charts and third-party widgets

  • Tables mirror automatically with dir, so the first column ends up on the right. Keep numeric columns right-aligned anyway (text-end = left in RTL; pick deliberately).
  • Charting libraries often need an explicit RTL option, such as reversed axes or a rtl: true legend. CSS-only bar charts built with flexbox mirror for free.
  • Date pickers: Persian sites usually need the Jalali (Solar Hijri) calendar. Intl.DateTimeFormat('fa-IR') formats dates in that calendar out of the box.

8. Testing checklist

  1. Use real text, not lorem ipsum. Persian words differ in length and shape, and layout bugs only show with real copy.
  2. Toggle direction live in DevTools (document.documentElement.dir = 'ltr'). If anything that shouldn't move breaks, you have a physical property somewhere.
  3. Search your CSS for left, right, margin-left, padding-right and translateX. Each one is a candidate bug.
  4. Check mobile off-canvas menus. They are the most common RTL animation bug.
  5. Test form validation with Persian digits and copy-pasted text.
  6. Screen reader pass. With the correct lang, VoiceOver and NVDA switch voices.

A free starter to skip the setup

I put all of the above into a small MIT-licensed template: a landing page (hero, features, pricing, FAQ) and a dashboard (off-canvas sidebar, stat cards, CSS bar chart, table). It uses Bootstrap 5 RTL, Vazirmatn and a Persian-digit helper, with no jQuery and no build step.

Clone it, change --brand in style.css, and start writing content. If you need complete production templates (shop, admin panel, corporate), we also maintain a collection of Persian RTL HTML templates at RTLHTML.

Got an RTL bug that isn't covered here? Drop it in the comments and I'll add it to the checklist.

Top comments (0)