RTL Text in Web Development: CSS Logical Properties and the Bidi Algorithm for Arabic, Hebrew, and Persian
A practical guide to building right-to-left layouts with CSS logical properties, the Unicode Bidirectional Algorithm, and bidi control characters — covering Arabic, Hebrew, and Persian.
RTL Text in Web Development: CSS Logical Properties and the Bidi Algorithm for Arabic, Hebrew, and Persian
Supporting right-to-left text is not just a checkbox on an accessibility audit. Arabic, Hebrew, and Persian together account for roughly 670 million native speakers (per Ethnologue 2024), and getting bidirectional layout wrong means broken reading flow, misaligned icons, and phone numbers that render digit-reversed. This guide walks through the four layers you need to get right: the HTML dir attribute, the Unicode Bidi Algorithm, CSS logical properties, and bidi control characters for edge cases.
Why the dir Attribute Is Your Foundation
The single most impactful line you can add to an Arabic or Hebrew page is:
<html lang="ar" dir="rtl">
Without dir="rtl", browsers use the Unicode Bidi Algorithm to guess direction from the first strong character in each paragraph, which is correct most of the time but fails silently on mixed-direction content. Setting dir explicitly on the root element cascades to every child and forces predictable results.
For a concrete example, consider this fragment without a direction hint:
<p>Contact: +972-50-000-0000 שלום</p>
Many browsers render this as:
שלום :Contact: +972-50-000-0000
The colon jumps because the bidi algorithm treats the punctuation as "neutral" and anchors it to the adjacent strong character. Adding dir="rtl" on the <p> gives the correct:
+972-50-000-0000 :שלום Contact:
(Hebrew reads right-to-left; the phone number remains left-to-right inside that RTL paragraph because digits are "weak" bidi characters and inherit the embedding direction from a run of LTR characters.)
I tested this on a Hebrew news site mockup and the only reliable fix was the explicit attribute — relying on automatic detection broke on every paragraph that started with a quotation mark or a number.
The Unicode Bidi Algorithm: 23 Character Types, One Rule Set
The Unicode Bidirectional Algorithm (UAX #9) assigns every code point one of 23 bidi character types. Three are called strong:
- L — Left-to-right (Latin letters, digits in LTR context)
- R — Right-to-left (Hebrew)
- AL — Arabic Letter (Arabic, Thaana, and others; treated differently from R in some contexts)
Everything else is weak (digits, currency symbols, punctuation) or neutral (spaces, control characters). The algorithm resolves direction runs by scanning these types, applying a set of rules, and then assigning an implicit embedding level to each character. Even without a dir attribute, a paragraph that starts with ش gets level 1 (RTL); one starting with A gets level 0 (LTR).
You do not need to implement UAX #9 yourself, but understanding the three strong types tells you why <span dir="ltr"> around a phone number works: it opens an explicit LTR embedding that overrides the surrounding RTL paragraph direction.
If you ever need to audit what bidi types a specific string contains — especially useful when debugging a paragraph where a lone punctuation mark flips — the Unicode Character Inspector tool shows the bidi category for every code point, no guessing required.
CSS Logical Properties: Write Once, Both Directions
The most common RTL breakage in LTR-first codebases is physical CSS: margin-left, padding-right, border-left, text-align: left. These properties are hard-coded to physical screen edges, so they do not mirror when you flip dir.
CSS Logical Properties solve this at the property level. Here is the same card component written both ways:
/* Physical — breaks in RTL */
.card {
padding-left: 1.25rem;
border-left: 4px solid #0066cc;
text-align: left;
margin-right: auto;
}
/* Logical — works in LTR and RTL without modification */
.card {
padding-inline-start: 1.25rem;
border-inline-start: 4px solid #0066cc;
text-align: start;
margin-inline-end: auto;
}
When dir="rtl" is set on an ancestor, padding-inline-start resolves to padding-right, border-inline-start resolves to border-right, and margin-inline-end resolves to margin-left — automatically. No media query, no [dir="rtl"] selector needed.
Browser support is strong: according to MDN's baseline data, CSS logical properties for flow-relative box alignment have been interoperable across Chrome, Firefox, and Safari since 2022 with no prefixes required. The only gap is in older iOS 14 and pre-2021 Samsung Browser, where inset-inline-start and inset-inline-end for positioning need a polyfill or fallback.
The mental model shift: inline-start is the reading start of a line (left in LTR, right in RTL); inline-end is the reading end; block-start is the top (regardless of writing mode); block-end is the bottom.
Bidi Control Characters: When Markup Is Not Enough
Sometimes you cannot wrap content in a <span dir="...">. This comes up in alt text, aria-label, email subjects, database-stored strings, and CSV exports. Unicode provides invisible bidi control characters for exactly these cases:
| Character | Code point | Purpose | |-----------|------------|---------| | LRM | U+200E | Left-to-Right Mark — acts like a strong L character | | RLM | U+200F | Right-to-Left Mark — acts like a strong R character | | LRE | U+202A | Left-to-Right Embedding (deprecated in favour of LRI) | | RLE | U+202B | Right-to-Left Embedding (deprecated) | | LRI | U+2066 | Left-to-Right Isolate | | RLI | U+2067 | Right-to-Left Isolate | | PDI | U+2069 | Pop Directional Isolate |
The modern recommendation is to use isolates (LRI/RLI + PDI) rather than the older embeddings (LRE/RLE) because isolates do not affect the bidi level outside their span. A minimal example: to force a Hebrew product name to display RTL inside an LTR string without markup:
input: "Buy פירות today"
output: "Buy פירות today" (פירות renders right-to-left, punctuation stays correct)
Without the RLI/PDI wrapper, the punctuation and space around the Hebrew word would resolve incorrectly depending on what follows.
These control characters are invisible and easy to accidentally strip when sanitizing input. When you suspect a bidi rendering bug in user-generated content, paste the text into the Unicode Character Inspector to make every zero-width and control character visible. For Arabic text specifically, run the string through the Unicode Normalizer as well — Arabic has presentation forms (U+FE70–U+FEFF) that look correct visually but break case-folding, searching, and BIDI analysis if they survive into your DOM.
Testing RTL Layouts Without Switching Your Entire OS
The fastest workflow I have found for RTL QA is a one-line browser DevTools trick:
document.documentElement.setAttribute('dir', 'rtl');
Paste that in the console and every CSS logical property on the page mirrors immediately. Physical properties (like margin-left) will break and stand out in the layout. This is faster than switching the browser language and does not require a real Arabic/Hebrew text corpus to see layout issues.
Beyond visual inspection, test:
- Keyboard navigation — Tab order should follow reading direction in RTL (right-to-left for Arabic/Hebrew forms).
- Number formatting — Phone numbers and model numbers should remain LTR inside RTL paragraphs.
- Icons and chevrons — Directional icons (arrows, forward/back chevrons) should flip. Non-directional icons (warning triangles, stars) should not.
- Truncation — CSS
text-overflow: ellipsisapplies at the visual end of the line. In RTL that is the left side, so truncation reads correctly without any adjustment.
A full RTL audit that covers all four layers — dir attributes, bidi algorithm overrides, logical properties, and control characters — takes a few hours on a medium-sized app but saves weeks of per-locale regression fixes later.
Made by Toolora · Updated 2026-06-27