In this article
Right-to-left writing systems represent some of the world's most widely spoken languages. Arabic alone has over 400 million native speakers across 22 countries, while Hebrew, Persian (Farsi), and Urdu together add hundreds of millions more. Building web typography that correctly supports RTL scripts requires understanding a distinct set of CSS properties, OpenType font features, and Unicode text algorithms that are entirely separate from Latin typesetting.
Unlike Latin script, Arabic and Hebrew are cursive scripts where letterforms change shape depending on their position within a word. An Arabic letter can have up to four forms: isolated, initial, medial, and final. The rendering engine must apply a sequence of OpenType substitution lookups to produce the correct connected glyphs from the underlying Unicode code points. Without a properly authored font and correct OpenType feature activation, Arabic text renders as a disconnected sequence of isolated characters rather than flowing, legible script.
Hebrew is somewhat simpler in that letters do not join, but it carries its own complexities: vowel marks (niqqud) appear as diacritics above and below consonants, cantillation marks are used in liturgical texts, and right-to-left directionality must coexist correctly with embedded LTR content such as numbers, URLs, and Latin words.
This guide covers everything you need to implement RTL typography correctly on the web: CSS properties for layout direction, the Unicode Bidirectional Algorithm for mixed-direction content, font selection for Arabic and Hebrew scripts, OpenType feature requirements, subsetting strategies, and testing approaches that catch the most common RTL implementation mistakes.
RTL Writing Systems Overview
The four major RTL writing systems each have distinct typographic requirements. Understanding their unique characteristics informs font selection, subsetting decisions, and rendering configurations.
Arabic Script
The Arabic Unicode block (U+0600–U+06FF) contains 256 code points covering the Arabic script used for Arabic, Pashto, Sindhi, Uyghur, and more. The Arabic Supplement (U+0750–U+077F) adds characters for additional languages. Arabic script is fully cursive: every letter connects to adjacent letters, and letterform varies by position (initial, medial, final, isolated). Correct rendering requires full HarfBuzz or CoreText shaping with activated OpenType GSUB tables.
Unicode blocks for Arabic:
- U+0600–U+06FF — Arabic (256 characters)
- U+0750–U+077F — Arabic Supplement (48 characters)
- U+FB50–U+FDFF — Arabic Presentation Forms-A
- U+FE70–U+FEFF — Arabic Presentation Forms-B
Hebrew Script
Hebrew occupies U+0590–U+05FF (112 code points) plus U+FB1D–U+FB4E in the Alphabetic Presentation Forms block. Hebrew letters do not join like Arabic; each letter stands independently. The 22 consonants are augmented by vowel points (niqqud, U+05B0–U+05C7) and cantillation marks (teamim, U+0591–U+05AF) that appear as combining characters. Modern web content typically omits niqqud except in educational, liturgical, or children's text.
Unicode blocks for Hebrew:
- U+0590–U+05FF — Hebrew (112 characters)
- U+FB1D–U+FB4E — Alphabetic Presentation Forms
Persian (Farsi) and Urdu
Persian and Urdu are written using the Perso-Arabic script, an extension of Arabic with additional letters for sounds not in classical Arabic. Persian adds four letters: پ (pe), چ (che), ژ (zhe), گ (gaf). Urdu adds several more, including ٹ (te), ڈ (dal), ڑ (re), ڻ (nun). These extended characters are in the Arabic block (U+0600–U+06FF) and Arabic Extended-A (U+08A0–U+08FF). Both languages use Nastaliq calligraphic style for formal typography, which requires specialized fonts and advanced shaping beyond standard Arabic rendering.
CSS Properties for RTL Layout
RTL layout requires a combination of HTML attributes and CSS properties working together. The most important decision is whether to use physical properties (left, right) or logical properties (inline-start, inline-end). For any site serving RTL languages, logical properties are strongly recommended—they adapt automatically when direction changes.
HTML dir Attribute
Set dir="rtl" on the <html> element for fully RTL pages, or on individual containers for RTL sections within an LTR page. The dir attribute cascades to all descendants and informs both CSS and the Unicode Bidi Algorithm.
<!-- Full RTL page (Arabic, Hebrew, Persian, Urdu) --> <html lang="ar" dir="rtl"> <head>...</head> <body>...</body> </html> <!-- RTL section within an LTR page --> <article lang="he" dir="rtl"> <!-- Hebrew content here --> </article> <!-- Inline RTL within LTR sentence --> <p>The word <span dir="rtl" lang="ar">مرحبا</span> means hello.</p>
CSS direction and unicode-bidi
The direction CSS property sets text direction for elements. It should always accompany the HTML dir attribute rather than replace it. The unicode-bidi property controls how the bidirectional algorithm handles an element. Use isolate to create a self-contained directionality context, preventing embedded content from affecting surrounding text direction.
/* RTL page baseline */
html[dir="rtl"] {
direction: rtl;
}
/* Isolate an RTL span within LTR prose */
.rtl-inline {
direction: rtl;
unicode-bidi: isolate;
}
/* Embed LTR within RTL (e.g., a URL or number) */
.ltr-embed {
direction: ltr;
unicode-bidi: isolate;
display: inline-block;
}CSS Logical Properties
Logical properties use flow-relative references (inline-start, inline-end, block-start, block-end) instead of physical directions (left, right, top, bottom). In an RTL document, inline-start maps to the right side, allowing the same CSS to work correctly in both LTR and RTL contexts without direction-specific overrides.
/* Physical (avoid for RTL-compatible layouts) */
.box {
margin-left: 1rem;
padding-right: 1.5rem;
border-left: 3px solid #f59e0b;
text-align: left;
}
/* Logical (works correctly in both LTR and RTL) */
.box {
margin-inline-start: 1rem;
padding-inline-end: 1.5rem;
border-inline-start: 3px solid #f59e0b;
text-align: start;
}
/* Logical properties for layout */
.nav {
float: inline-start; /* was: float: left */
inset-inline-start: 0; /* was: left: 0 */
inset-inline-end: auto; /* was: right: auto */
}Browser support: All modern browsers. IE11 and older do not support logical properties—use physical fallbacks if legacy support is required.
writing-mode for Vertical Scripts
While Arabic and Hebrew are horizontal RTL scripts, writing-mode becomes relevant if you work with Mongolian (vertical) or create mixed-axis layouts. For Arabic and Hebrew, writing-mode: horizontal-tb (the default) is correct. Do not set writing-mode: vertical-rl for Arabic—it will display incorrectly.
/* Correct for Arabic and Hebrew */
.arabic-text {
direction: rtl;
writing-mode: horizontal-tb; /* Default—no need to set */
}
/* Incorrect—never do this for Arabic/Hebrew */
.wrong {
writing-mode: vertical-rl; /* Rotates text incorrectly */
}Bidirectional Text Handling
Real-world Arabic, Hebrew, Persian, and Urdu content almost always contains bidirectional (bidi) text: RTL base content mixed with LTR sequences such as numbers, URLs, Latin brand names, email addresses, and code snippets. The Unicode Bidirectional Algorithm (UBA) handles most cases automatically, but complex mixed-direction content requires explicit markup to avoid rendering errors.
How the Unicode Bidi Algorithm Works
The UBA is built into every modern browser. It assigns directional properties to each Unicode character (strong LTR, strong RTL, neutral, or weak), then resolves the display order of runs of characters. Characters with intrinsic directionality (Arabic letters are strongly RTL, Latin letters are strongly LTR) determine the direction of their surrounding neutrals (punctuation, spaces).
The base paragraph direction (set by dir attribute) resolves ambiguity for neutral characters at the edges of directional runs. Without explicit markup, complex sequences can render in unexpected visual order even though the logical character order in the DOM is correct.
Embedding LTR Content in RTL Text
Wrap LTR content (numbers, URLs, Latin text) in a span with dir="ltr" and unicode-bidi: isolate to prevent the UBA from misinterpreting adjacent RTL context. Without isolation, parentheses and punctuation adjacent to the LTR content can flip to the wrong side.
<!-- RTL paragraph with embedded LTR URL -->
<p dir="rtl" lang="ar">
يمكنك زيارة الموقع على
<span dir="ltr" style="unicode-bidi: isolate;">
https://font-converters.com
</span>
للمزيد من المعلومات.
</p>
<!-- RTL text with embedded LTR number -->
<p dir="rtl" lang="ar">
السعر هو
<span dir="ltr" style="unicode-bidi: isolate;">$49.99</span>
فقط.
</p>Embedding RTL in LTR Text
Similarly, when embedding RTL words or phrases within an LTR document, use dir="rtl" and isolation to create a contained RTL context. This is common in multilingual glossaries, linguistics content, and documentation that discusses RTL scripts.
<!-- LTR paragraph with embedded Arabic word --> <p> The Arabic word for peace is <span dir="rtl" lang="ar" style="unicode-bidi: isolate;">سلام</span> (salam). </p> <!-- LTR paragraph with embedded Hebrew phrase --> <p> The Hebrew phrase <bdi lang="he">שלום עולם</bdi> means "hello world". </p>
The <bdi> element (Bidirectional Isolation) is semantically equivalent to a span with unicode-bidi: isolate and is the preferred semantic element for isolating text of unknown or different directionality.
Common Bidi Pitfalls
- Parentheses flipping: In RTL context, parentheses mirror visually—open and close swap sides, which is correct Unicode behavior. Use isolation to control which context they appear in.
- Number trailing punctuation: A period or comma after a number may appear on the wrong side without isolation. Always wrap standalone numbers in LTR spans within RTL containers.
- Mixed list items: List items with mixed-direction content should each have explicit
dirattributes rather than relying on the parent list direction. - Input fields: Use
dir="auto"on text inputs to detect direction from the first typed character—essential for multilingual forms.
Choosing RTL Fonts
Not all Arabic or Hebrew fonts render correctly in web environments. A production-quality RTL web font must contain complete OpenType GSUB and GPOS tables for shaping, cover the required Unicode blocks, and be optimized for screen rendering. The following fonts are proven choices for web use.
| Font Name | Script | Style | Weights | License | Notes |
|---|---|---|---|---|---|
| Noto Naskh Arabic | Arabic | Naskh serif | 400, 500, 600, 700 | OFL (free) | Best choice for body text; excellent vowel mark support |
| IBM Plex Arabic | Arabic | Modern sans-serif | 100–700 | OFL (free) | Pairs with IBM Plex Sans for bilingual UI |
| Cairo | Arabic | Geometric sans-serif | 200–900 | OFL (free) | Popular for modern Arabic web design; variable font available |
| Amiri | Arabic | Traditional Naskh serif | 400, 700 | OFL (free) | Ideal for literary, scholarly, and formal content |
| Tajawal | Arabic | Humanist sans-serif | 200–900 | OFL (free) | Clean and modern; good for news and editorial sites |
| Noto Sans Hebrew | Hebrew | Sans-serif | 100–900 | OFL (free) | Safe default; covers full Hebrew block including niqqud |
| Frank Ruhl Libre | Hebrew | Traditional serif | 300–900 | OFL (free) | Classic Hebrew newspaper style; excellent for long-form reading |
| Rubik | Hebrew + Latin | Rounded sans-serif | 300–900 | OFL (free) | Bilingual Hebrew/Latin; great for Israeli tech products |
| Heebo | Hebrew + Latin | Humanist sans-serif | 100–900 | OFL (free) | Versatile bilingual font; widely used for Israeli web projects |
Loading RTL Fonts with @font-face
Use unicode-range to restrict each RTL font to its script, preventing the browser from loading Arabic fonts on pages with no Arabic characters. Combine with font-display: swap for immediate text rendering during load.
/* Load Noto Naskh Arabic only for Arabic characters */
@font-face {
font-family: 'Noto Naskh Arabic';
src: url('/fonts/NotoNaskhArabic-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0600-06FF, U+0750-077F, U+08A0-08FF,
U+FB50-FDFF, U+FE70-FEFF;
}
@font-face {
font-family: 'Noto Naskh Arabic';
src: url('/fonts/NotoNaskhArabic-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
unicode-range: U+0600-06FF, U+0750-077F, U+08A0-08FF,
U+FB50-FDFF, U+FE70-FEFF;
}
/* Bilingual Arabic/Latin font stack */
body:lang(ar) {
font-family: 'Noto Naskh Arabic', 'Times New Roman', serif;
}
/* Bilingual Hebrew/Latin font stack */
body:lang(he) {
font-family: 'Heebo', 'Arial', sans-serif;
}OpenType Features for Arabic
Arabic script rendering relies entirely on OpenType GSUB (Glyph Substitution) and GPOS (Glyph Positioning) tables. The browser's text shaping engine (HarfBuzz on Chrome/Firefox, CoreText on Safari) applies these features automatically for content with lang="ar". However, understanding which features are active helps diagnose rendering issues and enables advanced typographic control.
| Feature Tag | Name | Description |
|---|---|---|
| calt | Contextual Alternates | Selects context-sensitive glyph alternates for correct letter connections. Fundamental for all Arabic fonts. |
| init | Initial Forms | Substitutes glyphs for letters at the start of a word that connect to the next letter. |
| medi | Medial Forms | Substitutes glyphs for letters in the middle of a word, connected on both sides. |
| fina | Final Forms | Substitutes glyphs for letters at the end of a word, connected only on the right (in RTL flow). |
| isol | Isolated Forms | Substitutes the standalone form for letters that cannot connect on either side. |
| mark | Mark Positioning | Positions combining marks (vowel points, hamza, shadda) relative to their base glyph using GPOS anchors. |
| mkmk | Mark-to-Mark Positioning | Positions combining marks relative to other combining marks—required for stacked vowels and diacritics. |
| liga | Standard Ligatures | Combines letter pairs into single ligature glyphs. The lam-alef (لا) ligature is mandatory in Arabic. |
| rlig | Required Ligatures | Ligatures that are mandatory for correct text rendering and cannot be disabled by the user. |
Controlling Arabic OpenType Features in CSS
Most Arabic shaping features are applied automatically by the browser when the language is set correctly. Manual feature control is useful for enabling optional stylistic variants or debugging rendering issues.
/* Arabic text — shaping features applied automatically with lang="ar" */
[lang="ar"] {
font-family: 'Noto Naskh Arabic', serif;
direction: rtl;
/* Enable optional features */
font-feature-settings:
"calt" 1, /* Contextual alternates (default on) */
"liga" 1, /* Standard ligatures (default on) */
"rlig" 1, /* Required ligatures (always on) */
"mark" 1, /* Mark positioning (default on) */
"mkmk" 1; /* Mark-to-mark (default on) */
}
/* Enable kashida (Arabic justification extension) if font supports it */
[lang="ar"].kashida-enabled {
font-feature-settings: "calt" 1, "liga" 1, "rlig" 1,
"mark" 1, "mkmk" 1, "kern" 1;
text-align: justify;
text-justify: kashida; /* Experimental—check browser support */
}
/* Optional decorative alternates for headings */
.arabic-heading {
font-feature-settings: "calt" 1, "rlig" 1, "ss01" 1; /* Stylistic set 1 */
}Subsetting RTL Fonts
Full Arabic and Hebrew fonts can be 150–300KB even in WOFF2 format, because they contain hundreds of glyph variants for each positional form, optional decorative alternates, and extended Unicode coverage. Subsetting to only the needed Unicode ranges reduces this to 50–100KB without affecting rendering quality for typical web content.
Unicode Ranges for Subsetting
Use these ranges in both CSS unicode-range descriptors and your subsetting tool (pyftsubset, glyphhanger, or Subsetter.io) to target only the characters needed.
/* Arabic @font-face with unicode-range */
@font-face {
font-family: 'Cairo';
src: url('/fonts/cairo-arabic.woff2') format('woff2');
font-weight: 400;
font-display: swap;
unicode-range:
U+0600-06FF, /* Arabic block (256 chars) */
U+0750-077F, /* Arabic Supplement (48 chars) */
U+08A0-08FF, /* Arabic Extended-A */
U+FB50-FDFF, /* Arabic Presentation Forms-A */
U+FE70-FEFF, /* Arabic Presentation Forms-B */
U+0660-0669, /* Arabic-Indic numerals */
U+060C, /* Arabic comma */
U+061B, /* Arabic semicolon */
U+061F, /* Arabic question mark */
U+0640; /* Arabic tatweel (kashida) */
}
/* Hebrew @font-face with unicode-range */
@font-face {
font-family: 'Heebo';
src: url('/fonts/heebo-hebrew.woff2') format('woff2');
font-weight: 400;
font-display: swap;
unicode-range:
U+0590-05FF, /* Hebrew block (112 chars) */
U+FB1D-FB4E, /* Hebrew Presentation Forms */
U+20AA, /* New Shekel sign */
U+05F3-05F4; /* Hebrew punctuation geresh/gershayim */
}Subsetting with pyftsubset
The pyftsubset tool from FontTools creates subsetted font files from the command line. Critical: always include --layout-features=* to preserve all OpenType GSUB/GPOS tables required for Arabic shaping.
# Subset an Arabic font—MUST include all layout features pyftsubset NotoNaskhArabic-Regular.ttf --unicodes="U+0600-06FF,U+0750-077F,U+08A0-08FF, U+FB50-FDFF,U+FE70-FEFF,U+0660-0669, U+060C,U+061B,U+061F,U+0640" --layout-features="*" --flavor=woff2 --output-file=NotoNaskhArabic-ar-subset.woff2 # Subset a Hebrew font pyftsubset Heebo-Regular.ttf --unicodes="U+0590-05FF,U+FB1D-FB4E,U+20AA,U+05F3-05F4" --layout-features="*" --flavor=woff2 --output-file=Heebo-he-subset.woff2
Critical warning
Never use --layout-features="" or omit layout features when subsetting Arabic fonts. Doing so strips the GSUB tables that enable contextual shaping, resulting in disconnected, unreadable Arabic text.
Expected File Sizes After Subsetting
| Font | Full WOFF2 | Subsetted WOFF2 | Reduction |
|---|---|---|---|
| Noto Naskh Arabic Regular | ~180KB | ~75KB | 58% |
| Cairo Regular | ~150KB | ~60KB | 60% |
| Amiri Regular | ~300KB | ~100KB | 67% |
| Heebo Regular | ~90KB | ~35KB | 61% |
| Frank Ruhl Libre Regular | ~110KB | ~40KB | 64% |
Testing RTL Layouts
RTL typography bugs are often invisible to developers who only test with LTR content. A systematic testing approach with real RTL text catches layout mirroring issues, bidi rendering bugs, and font shaping failures before they reach production.
Common RTL Pitfalls
- ✗Physical CSS properties not mirrored: Using
padding-leftinstead ofpadding-inline-startcreates reversed padding in RTL contexts. Audit all physical directional properties. - ✗Icons and arrows not mirrored: Directional icons (back arrows, next arrows, progress indicators) should flip in RTL. Use CSS
transform: scaleX(-1)or provide mirrored SVG variants for directional glyphs. - ✗Missing lang attribute: Without
lang="ar"orlang="he", the shaping engine may not apply the correct OpenType feature lookups, producing degraded glyph selection even with a good font. - ✗Hardcoded left-aligned text:
text-align: leftoverrides the browser's natural RTL alignment. Usetext-align: startto respect the content's writing direction. - ✗Subsetted font missing GSUB tables: Subsetting Arabic without
--layout-features=*strips shaping tables, producing disconnected letters. Always verify rendering after subsetting. - ✗Scroll position jumping on direction change: Switching direction on the
<html>element can cause scroll position issues. Handle language switches carefully and reset scroll position if needed.
RTL Testing Checklist
Layout
- ☐ Navigation appears on correct side
- ☐ Sidebar position is mirrored
- ☐ Flex/grid layouts reverse correctly
- ☐ Icons and arrows are mirrored
- ☐ Form labels align to correct side
- ☐ Breadcrumbs read right to left
Typography
- ☐ Arabic letters join correctly (no disconnection)
- ☐ Vowel marks (harakat) position correctly
- ☐ Numbers display in correct order
- ☐ Mixed bidi content renders in correct order
- ☐ Font loads (check DevTools Network tab)
- ☐ Fallback font renders acceptably
Interaction
- ☐ Text input fields accept RTL input
- ☐ Cursor moves correctly in RTL fields
- ☐ Copy-paste preserves text direction
- ☐ Screen reader announces content order
Performance
- ☐ RTL font loads in under 500ms
- ☐ No layout shift on font swap
- ☐ unicode-range prevents unnecessary loads
- ☐ Font is preloaded for above-fold text
Test Strings for Arabic and Hebrew
Use these strings to verify font rendering. Correct Arabic rendering shows connected, contextually shaped letters; correct Hebrew rendering shows properly positioned diacritics.
<!-- Arabic test strings --> <!-- Basic connectivity: all letters should join --> <p lang="ar" dir="rtl">بسم الله الرحمن الرحيم</p> <!-- Lam-alef ligature (mandatory): لا must merge into single glyph --> <p lang="ar" dir="rtl">لا إله إلا الله</p> <!-- Mixed bidi: Arabic with embedded Latin and number --> <p lang="ar" dir="rtl">السعر هو <span dir="ltr">USD 49.99</span> فقط</p> <!-- Hebrew test strings --> <!-- Basic Hebrew consonants --> <p lang="he" dir="rtl">שלום עולם</p> <!-- Hebrew with niqqud (vowel points) --> <p lang="he" dir="rtl">בְּרֵאשִׁית בָּרָא אֱלֹהִים</p> <!-- Hebrew with embedded LTR URL --> <p lang="he" dir="rtl">בקר באתר <bdi>https://font-converters.com</bdi></p>
Right-to-Left Font FAQs
Common questions about Arabic, Hebrew, and RTL web typography
Written & Verified by
Sarah Mitchell
Product Designer, Font Specialist
Related Resources
Subset Arabic Fonts
Step-by-step guide to subsetting Arabic web fonts
Arabic Unicode Converter
Convert and inspect Arabic Unicode code points
Font Subsetter Tool
Subset any font file online for free
Multilingual Font Setup
Configure fonts for multi-language websites
Font Fallback Chains
Build robust font stacks for RTL and LTR scripts
Ready to Subset Your Arabic or Hebrew Font?
Use our Font Subsetter to reduce Arabic font sizes from 150–300KB to 50–100KB while preserving all OpenType shaping features.
Open Font Subsetter