
How to Handle Right-to-Left (RTL) Typography in CSS?
- Sajjad
- Typography
- 01 Oct, 2026
The Arabic version of the site launches, and the bug reports arrive within an hour. Paragraphs are right-aligned, but the bullet points sit on the wrong side of the list. Icons with arrows point backward. A phone number in the footer reads in the wrong order. Headings that use the brand's slight letter spacing now show broken, disconnected letters. Someone fixed a few issues with float: right overrides, and now the English pages are broken too. All of these problems come from treating RTL as "flip the text alignment" rather than as a different writing direction the whole layout must respect.
This article covers how to set document direction correctly, how to use CSS logical properties so one stylesheet serves both directions, how to handle mixed-direction text and numbers, and the typographic adjustments Arabic and Hebrew script actually need, with examples in plain CSS, Tailwind CSS v4, and Next.js.
What RTL Means for a Web Page
Right-to-left (RTL) scripts are written starting from the right edge of the line. The major RTL languages on the web are Arabic, Hebrew, Persian (Farsi), and Urdu, plus others such as Pashto, Sindhi, Kurdish (Sorani), and Yiddish.
Two ideas make RTL work in browsers:
- Base direction is the overall direction of a block of text and its layout: which side text starts on, where list bullets go, which way inline content flows. You set it with the
dirattribute. - The Unicode Bidirectional Algorithm (bidi) decides the visual order of characters within a line when directions mix. Arabic letters are strongly right-to-left, Latin letters strongly left-to-right, and digits and punctuation are weak or neutral, taking direction from their surroundings.
Text is stored in logical order, the order it is typed and read, and the browser displays it in visual order. You should never reverse strings or reorder characters yourself.
Step 1: Set dir and lang in HTML, Not CSS
Set direction on the root element with the dir attribute, alongside lang:
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8" />
<title>من نحن</title>
</head>
<body>
...
</body>
</html>
Do not rely on the CSS direction property for document direction. Direction is part of the content's meaning, not its presentation. If CSS fails to load, or the content is read by a feed reader, translation tool, or screen reader, the dir attribute still works. lang is equally important: it controls font selection for shared characters, hyphenation, and pronunciation by assistive technology.
In a Next.js App Router project with a locale segment, set both in the root layout:
// src/app/[locale]/layout.tsx
const rtlLocales = new Set(["ar", "he", "fa", "ur"]);
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const dir = rtlLocales.has(locale.split("-")[0]) ? "rtl" : "ltr";
return (
<html lang={locale} dir={dir}>
<body>{children}</body>
</html>
);
}
For the routing side of locales, see how Next.js supports internationalization.
Step 2: Replace Physical Properties with Logical Ones
Physical properties like margin-left and text-align: right describe screen sides. Logical properties describe positions relative to the writing direction: start is where a line begins, end is where it finishes, inline is the direction of text, and block is the direction paragraphs stack.
| Physical property | Logical equivalent | In LTR means | In RTL means |
|---|---|---|---|
margin-left | margin-inline-start | left | right |
padding-right | padding-inline-end | right | left |
border-left | border-inline-start | left | right |
left: 0 | inset-inline-start: 0 | left | right |
text-align: left | text-align: start | left | right |
float: right | float: inline-end | right | left |
border-top-left-radius | border-start-start-radius | top left | top right |
margin-top | margin-block-start | top | top |
With logical properties, one rule works in both directions:
.callout {
border-inline-start: 4px solid var(--color-accent);
padding-inline: 1.25rem 1rem;
padding-block: 1rem;
text-align: start;
}
.article ul {
padding-inline-start: 1.5rem;
}
.badge {
position: absolute;
inset-block-start: 0.5rem;
inset-inline-end: 0.5rem;
}
Flexbox and Grid already follow the inline direction. A row of items with display: flex starts on the right in an RTL document without any extra code, and justify-content: flex-start means "start of the line" in either direction. Use gap for spacing between items rather than margins on one side.
Logical properties have been supported in all major browsers for years, so there is no reason to maintain a separate RTL stylesheet or run a build-time flipping tool on new projects.
Step 3: Use the RTL Variants in Tailwind CSS v4
Tailwind v4 ships logical utilities and direction variants. Prefer the logical utilities:
| Physical | Logical utility | Property |
|---|---|---|
ml-4 | ms-4 | margin-inline-start |
mr-4 | me-4 | margin-inline-end |
pl-6 | ps-6 | padding-inline-start |
pr-6 | pe-6 | padding-inline-end |
left-0 | start-0 | inset-inline-start |
text-left | text-start | text-align: start |
border-l-4 | border-s-4 | border-inline-start-width |
rounded-l-lg | rounded-s-lg | start corners |
<aside class="border-s-4 border-amber-400 ps-5 pe-4 text-start">
<p class="text-lg">...</p>
</aside>
When something genuinely must differ by direction, such as an icon transform, use the rtl: and ltr: variants:
<a href="/next" class="inline-flex items-center gap-2">
التالي
<svg class="size-4 rtl:-scale-x-100" aria-hidden="true">...</svg>
</a>
Step 4: Mirror Icons Selectively
Not every icon should flip. Mirror icons that represent direction or sequence, and leave icons that represent real-world objects alone.
- Mirror: back and forward arrows, "next" chevrons, progress indicators, undo and redo, reply, text-indent icons, list bullets with direction.
- Do not mirror: media play buttons (they follow tape direction, conventionally unchanged), clocks, checkmarks, search magnifiers, logos, and icons containing text or numbers.
In plain CSS, the :dir() pseudo-class matches an element's computed direction, including direction inherited from an ancestor:
.icon-directional:dir(rtl) {
transform: scaleX(-1);
}
:dir(rtl) is more reliable than [dir="rtl"] .icon, because it also works for elements inside components that do not know where the dir attribute was set, and for elements whose direction comes from dir="auto".
Step 5: Isolate Mixed-Direction Text
RTL pages constantly contain LTR fragments: brand names, URLs, email addresses, code, product SKUs, and numbers. The bidi algorithm handles simple cases, but neutral characters at the boundaries, such as punctuation, parentheses, and hyphens, are often placed on the wrong side.
The fix is isolation: treat a fragment as a self-contained unit with its own direction.
<p>
تواصل معنا عبر
<bdi>support@example.com</bdi>
أو اتصل على
<span dir="ltr">+1 (555) 010-2030</span>.
</p>
<ul>
<li>المستخدم: <bdi>{user.name}</bdi></li>
</ul>
- The
bdielement isolates content whose direction is unknown, such as user names from a database. Its direction is detected from the content. dir="ltr"on a span forces a fragment like a phone number to display left to right, so the digits, parentheses, and hyphens stay in order.dir="auto"on any element detects direction from the first strong character. Use it on user-generated content blocks, comments, and form fields.
In CSS, the equivalent tool is unicode-bidi:
.user-content {
unicode-bidi: plaintext;
}
.sku,
.code-inline {
direction: ltr;
unicode-bidi: isolate;
}
plaintext makes each paragraph determine its own direction from its content, which is ideal for comments that may be written in Arabic or English. isolate prevents a fragment from affecting the text around it.
Form Fields
Inputs need direction too:
<input type="text" name="name" dir="auto" />
<input type="email" name="email" dir="ltr" />
<input type="url" name="website" dir="ltr" />
<input type="tel" name="phone" dir="ltr" />
Email, URL, and phone values are always LTR, so set that explicitly. Free-text fields get dir="auto" so they follow whatever the user types. Keep the label aligned with the page direction.
Step 6: Apply RTL-Specific Typography Rules
Logical properties fix layout. Arabic and Hebrew also need different typographic settings than Latin text. These are the rules that matter most.
Never Letter-Space Arabic Script
Arabic, Persian, and Urdu letters connect to each other. Adding letter-spacing inserts gaps between joined letters and breaks the connections, producing text that looks broken and is harder to read. Remove any tracking inherited from Latin styles:
:lang(ar),
:lang(fa),
:lang(ur) {
letter-spacing: 0;
}
Browsers have improved at preserving joins when letter spacing is applied, but behavior is still not something to rely on, and positive tracking is not part of Arabic typographic convention anyway. Hebrew does not join, so light tracking on Hebrew headings is acceptable, but avoid all-caps-style tracking habits. The general effects of tracking are covered in what tracking is in typography.
Use Bold or Color Instead of Italics
Arabic and Hebrew have no traditional italic. Most Arabic fonts have no italic style, so font-style: italic produces a synthetic slant that looks wrong. Replace emphasis styles:
:lang(ar) :is(em, i, cite),
:lang(fa) :is(em, i, cite),
:lang(he) :is(em, i, cite) {
font-style: normal;
font-weight: 700;
}
Increase Size and Line Height
At the same nominal font size, Arabic text usually appears smaller than Latin text because more of each glyph's vertical space is used for ascenders, descenders, and dots. Arabic also has tall vertical extents that collide at tight line heights. Practical starting points:
| Setting | Latin body | Arabic body | Hebrew body |
|---|---|---|---|
| Font size | 1rem | 1.0625–1.125rem | 1rem–1.0625rem |
| Line height | 1.5–1.6 | 1.7–1.9 | 1.6–1.7 |
| Letter spacing | 0 or small | 0 | 0 |
| Italic emphasis | Yes | No | No |
:lang(ar) {
font-family: "Noto Naskh Arabic", "IBM Plex Sans Arabic", system-ui, sans-serif;
font-size: 1.0625rem;
line-height: 1.8;
}
Adjust Underlines
Arabic letters have dots and descenders below the baseline, so default underlines cut through them. Push the underline down and let it skip glyphs:
:lang(ar) a {
text-underline-offset: 0.35em;
text-decoration-skip-ink: auto;
text-decoration-thickness: 0.06em;
}
Do Not Justify by Default
Traditional Arabic justification stretches connections between letters (kashida) rather than widening spaces. Browser support for kashida-based justification is limited, so text-align: justify usually produces wide, uneven word gaps. Use text-align: start.
Step 7: Choose Fonts Designed for RTL Scripts
Pick fonts drawn by designers who know the script, with complete coverage and proper OpenType shaping. Reliable choices on Google Fonts include:
- Arabic: Noto Naskh Arabic and Noto Sans Arabic, IBM Plex Sans Arabic, Cairo, Tajawal, Almarai, Readex Pro
- Persian: Vazirmatn (which includes Persian-specific forms and digits)
- Urdu: Noto Nastaliq Urdu (Nastaliq needs generous line height, often 2 or more)
- Hebrew: Heebo, Assistant, Rubik, Noto Sans Hebrew, Frank Ruhl Libre
Persian and Urdu use Arabic script but have additional letters and different preferred forms, so an "Arabic" font is not automatically right for them. Check coverage for each language, and set lang="fa" or lang="ur" so localized forms apply. For a broader approach to multiple scripts on one site, see choosing fonts for multilingual websites.
Digits
Arabic-script languages use different digit sets. Many Arabic sites in the Middle East use Arabic-Indic digits (٠١٢٣), Persian uses Extended Arabic-Indic digits (۰۱۲۳), and North African sites typically use Western digits (0123). Which digits appear is decided by your content and formatting code, not by CSS. Use Intl.NumberFormat with the correct locale and make sure your font includes the digit set you output:
new Intl.NumberFormat("ar-EG").format(2026); // "٢٠٢٦"
new Intl.NumberFormat("fa-IR").format(2026); // "۲۰۲۶"
new Intl.NumberFormat("ar-MA").format(2026); // "2026"
Step 8: Test RTL Properly
- Flip an English page by setting
dir="rtl"onhtmlin dev tools. Layout problems show up immediately, even before translations exist. - Test with real content from native speakers. Placeholder Arabic generated by a translation tool rarely includes realistic mixed-direction strings.
- Check mixed strings: emails, URLs, prices, dates, phone numbers, and product names inside RTL sentences.
- Check components like carousels, sliders, progress bars, and tooltips, which often use physical positioning in JavaScript.
- Check with a screen reader in Arabic or Hebrew to confirm
langis set and reading order is logical.
RTL Typography FAQ
Use the dir attribute in HTML. Direction is part of the document's meaning, so it must work even when CSS does not load. Reserve the CSS direction property for isolated technical cases, such as forcing a code fragment to display left to right.
Not if you use CSS logical properties. Properties such as margin-inline-start and text-align start adapt automatically to the document's direction, so one stylesheet serves both LTR and RTL pages.
Arabic letters connect to their neighbors, and letter spacing inserts gaps between those joined letters. The result looks broken and is harder to read, so set letter spacing to zero for Arabic, Persian, and Urdu.
Wrap them in an element with dir set to ltr, or use the bdi element for content with unknown direction. Isolation stops neutral characters such as parentheses and hyphens from being reordered by the surrounding right-to-left text.
Mirror icons that show direction or sequence, such as arrows, chevrons, back and forward buttons, and progress indicators. Leave icons of real-world objects, logos, checkmarks, clocks, and media play buttons unmirrored.
Usually slightly. Arabic tends to look smaller at the same nominal size, so many sites set Arabic body text five to twelve percent larger with a line height between 1.7 and 1.9.
Conclusion
Handling RTL typography well starts with structure. Set dir and lang on the root element, write layout with logical properties or Tailwind's logical utilities, and let Flexbox and Grid follow the writing direction. Mirror only icons that represent direction, and isolate LTR fragments like emails, phone numbers, and user names with bdi, dir="ltr", or dir="auto".
Then adjust the type itself. Remove letter spacing from Arabic-script text, replace italics with bold, increase size and line height, push underlines below the dots, avoid justification, and choose fonts designed for the specific language. Test with real content from native speakers. With those pieces in place, one codebase produces Arabic, Hebrew, Persian, and Urdu pages that read as naturally as the English ones.
Here are some useful references for going deeper on RTL typography:
- W3C Internationalization: Structural markup and right-to-left text in HTML — authoritative guidance on
dir, isolation, and bidi markup. - MDN Web Docs: CSS logical properties and values — every logical property and how it maps to physical sides.
- MDN Web Docs: :dir() — matching elements by computed direction.
- Tailwind CSS Docs: Hover, focus, and other states — includes the
rtlandltrvariants. - MDN Web Docs: unicode-bidi — isolate, plaintext, and other bidi values.


