Two years ago we shipped our first Arabic-language product. Today we run four: this blog, Arabic Teleprompter, edar.ae, and helpyard.ae. Each one taught us something different about RTL development. What follows is everything we wish we had read before we started.
The biggest lesson, and the framing for all of this: build RTL first, not RTL last. Every project where RTL was the primary direction from day one was easier to ship. Every project where we added Arabic “later” cost us significant rework. RTL-first is not just a preference — it is an architecture decision.
The Difference Between RTL Support and RTL-First
Most “RTL support” guides teach you how to add RTL to an existing LTR codebase. That is the wrong problem to solve. By the time you add RTL to an existing system, you have already made dozens of assumptions — about icon direction, about spacing rhythms, about component hierarchy — that feel natural in LTR and fight you in RTL.
RTL-first means designing and building the Arabic experience as the primary reference, and deriving LTR from it. In practice this looks like:
- Writing component CSS using logical properties (
inline-start,block-end) before ever testing in LTR - Composing layouts with flex and grid that respond to
dirattribute changes without component modifications - Testing new components in Arabic before English, because Arabic text is longer per concept and surfaces overflow problems immediately
On the Arabic Teleprompter specifically, the entire interface was designed and shipped in Arabic before English was layered in. The result: zero RTL bugs after launch. Compare that to edar.ae, where we built English first and spent a full week resolving 47 layout issues when Arabic was added.
CSS Logical Properties: The Real Foundation
If there is one technical change that matters above everything else in RTL web development, it is switching to CSS logical properties completely. Not selectively. Not where it is convenient. Completely.
The logical property model maps physical directions (left/right/top/bottom) to text-flow-relative directions (inline-start, inline-end, block-start, block-end). In LTR, inline-start is left. In RTL, it is right. The browser handles the swap — you write the rule once.
/* Physical — breaks RTL */
.nav-icon {
margin-right: 0.5rem;
}
/* Logical — works in both directions */
.nav-icon {
margin-inline-end: 0.5rem;
}
The full mapping:
| Physical | Logical Equivalent |
|---|---|
margin-left | margin-inline-start |
margin-right | margin-inline-end |
padding-left | padding-inline-start |
padding-right | padding-inline-end |
left (position) | inset-inline-start |
right (position) | inset-inline-end |
border-left | border-inline-start |
text-align: left | text-align: start |
Browser support for all of these is excellent — they are baseline features in every browser since 2023. There is no progressive enhancement argument against using them.
The one that trips everyone up: text-align: start vs. text-align: left. In LTR they produce identical output. In RTL, text-align: start aligns text to the right — which is almost always what you want for Arabic body copy. text-align: left in an RTL context looks immediately wrong to native readers.
What Logical Properties Don’t Cover
Logical properties handle spacing and positioning, but they do not handle visual direction of icons, chevrons, or any UI element that carries inherent directional meaning.
A forward arrow → should point right in LTR and left in RTL (toward the reading direction). The conventional approach is a CSS transform:
[dir="rtl"] .directional-icon {
transform: scaleX(-1);
}
We wrap this in a utility class in all our projects:
.icon-directional {
display: inline-block;
}
[dir="rtl"] .icon-directional {
transform: scaleX(-1);
}
Icons that represent physical objects (a camera, a microphone, a checkmark) do not need to be mirrored. Only icons with inherent directionality (arrows, back/forward, progress indicators) should be flipped.
Bidirectional Text: The Genuinely Hard Part
RTL layout is mechanical and solvable with the patterns above. Bidirectional text — mixed Arabic and English in the same paragraph, line, or even word — is harder. Unicode’s bidi algorithm handles the rendering, but it makes assumptions that sometimes produce the wrong result.
The most common failure mode: an Arabic sentence that ends with a URL, product name, or technical term in English. The Unicode bidi algorithm will treat the English run as LTR, which is correct — but the punctuation around it (colon, parenthesis, period) may end up in an unexpected position depending on where it sits in the surrounding RTL run.
Example: هذا هو الموقع: alsheikhmedia.com. renders with the period before the URL in some browsers, because the period is ambiguous — it could belong to the Arabic run or the English run.
The fix is to use Unicode bidi control characters explicitly:
<!-- Wrap embedded LTR content in a bidi isolate span -->
<p>هذا هو الموقع: <span dir="ltr">alsheikhmedia.com</span>.</p>
The dir="ltr" on the inline span creates a bidi isolation boundary. The period now unambiguously belongs to the Arabic sentence. We apply this automatically to every URL, code token, and product name that appears inside Arabic prose in our CMS rendering pipeline.
Numbers in Arabic Context
Arabic-speaking regions use two numeral systems: Arabic-Indic numerals (٠١٢٣٤٥٦٧٨٩) and Western Arabic numerals (0–9). Gulf markets — UAE, Saudi Arabia, Kuwait — predominantly use Western numerals in digital interfaces. Do not assume you need to convert to Arabic-Indic numerals just because the UI is in Arabic.
However: number punctuation conventions differ. Thousands separators in Arabic contexts often use . where English uses ,, and vice versa. If you are formatting numbers, do not hardcode punctuation — use the Intl.NumberFormat API with the locale explicitly set:
// Gulf Arabic locale (uses Western numerals, Gulf punctuation conventions)
const formatter = new Intl.NumberFormat('ar-AE', {
useGrouping: true,
});
formatter.format(1500000); // Returns "1,500,000" in Gulf Arabic context
Astro Component Architecture for RTL
We use Astro for all our current properties. The RTL-first approach shapes how we write components at every level.
The Layout Shell
All pages pass lang and dir down to the root <html> element. We derive dir from lang — never store it separately, because having two props that can contradict each other is a source of bugs.
---
// Layout.astro
const { lang } = Astro.props;
const dir = lang === 'ar' ? 'rtl' : 'ltr';
const fontClass = lang === 'ar' ? 'font-arabic' : 'font-latin';
---
<html lang={lang} dir={dir} class={fontClass}>
<head>
<!-- ... -->
</head>
<body>
<slot />
</body>
</html>
The fontClass is important. Arabic and Latin typefaces have different line-height, letter-spacing, and weight rendering characteristics. We use separate CSS classes to set per-script typographic defaults rather than trying to handle it with a single set of values.
Typography Configuration
In our global CSS, we define script-specific typographic defaults:
/* Latin defaults */
.font-latin {
font-family: 'Inter', system-ui, sans-serif;
line-height: 1.6;
letter-spacing: -0.01em;
}
/* Arabic defaults — Cairo typeface, higher line height, no letter spacing */
.font-arabic {
font-family: 'Cairo', 'Noto Sans Arabic', sans-serif;
line-height: 1.8;
letter-spacing: 0; /* Never apply letter-spacing to Arabic */
}
The no-letter-spacing rule for Arabic is absolute. Arabic text forms ligatures — characters that connect and reshape based on their neighbors. Letter spacing breaks those connections and produces text that looks like individual disconnected letters, which is both visually incorrect and degrading to readability. The Cairo font handles ligature rendering automatically; your job is to not break it.
Component Composition
Components that need to respond to text direction should use CSS logical properties in their stylesheets rather than JavaScript conditionals. Avoid this pattern:
<!-- Fragile — requires passing direction into every component -->
<Card direction={dir} />
Instead, write the component’s CSS with logical properties and let the inherited dir attribute on <html> do the work:
/* card.css — works in both directions */
.card {
padding-inline: 1.5rem;
padding-block: 1rem;
border-inline-start: 3px solid var(--accent);
}
The border-inline-start will automatically appear on the left in LTR and the right in RTL. No JavaScript. No prop threading. The CSS inheritance does it.
Content Collections with Language Routing
Our content directory structure for bilingual blogs:
src/content/blog/
├── en/
│ └── post-slug.md
└── ar/
└── post-slug.md
Each file’s frontmatter includes lang: en or lang: ar, which the layout reads directly. The routing in src/pages/ creates /en/blog/[slug].astro and /ar/blog/[slug].astro as separate static routes. A language switcher links between the two using the slug as a shared key.
This is a content-first approach: every post must exist in both languages before publishing, and the slug is the canonical identifier across both. No translation is an implicit no-publish.
Lessons from Specific Products
Arabic Teleprompter
The teleprompter’s core challenge: scrolling Arabic text at a configured WPM rate, with clean bidirectional handling when scripts are mixed. Arabic text runs right-to-left, but the scroll direction is the same (downward) regardless of language.
The tricky part was the editing interface. Users paste Arabic content, sometimes with embedded English brand names, sometimes with punctuation like (, :, or —. We had multiple bug reports where the cursor position after a mixed-direction paste was wrong, or where the selection highlight didn’t span the expected characters.
The fix was using a contenteditable element with explicit unicode-bidi: isolate on inline spans wrapping LTR content:
.teleprompter-content [dir="ltr"] {
unicode-bidi: isolate;
}
unicode-bidi: isolate is stronger than dir="ltr" alone — it tells the bidi algorithm to treat the element as a completely isolated bidi context, preventing the outer RTL context from affecting the rendering of the inner LTR content.
edar.ae
Edar is a property management platform. Real estate in the UAE is inherently bilingual — contracts are in Arabic, listings appear in both languages, users switch contexts mid-session.
The lesson from edar: form inputs are where RTL breaks most visibly. An <input type="text"> inherits dir from its parent, but text entry in Arabic — using a keyboard that switches directions — can produce unexpected cursor behavior if the input’s rendering direction doesn’t match the entry direction.
Our solution: dynamically set dir on text inputs based on the language of the text being entered.
input.addEventListener('input', (e) => {
const value = e.target.value;
// If the first character is in the Arabic Unicode range, set RTL
const isArabic = /[\u0600-\u06FF]/.test(value[0]);
e.target.dir = isArabic ? 'rtl' : 'ltr';
});
This small snippet resolves the most common complaint Arabic-speaking users have with forms: that the cursor starts on the wrong side.
helpyard.ae
Helpyard is a service marketplace. Users browse and book household services, and the interface must feel native in both Arabic and English because both languages are used daily by the same users.
The main challenge was icon and image directionality in a card grid. When we flipped the layout to RTL, photographs of people (which have an inherent LTR visual flow in stock photography) felt reversed. A photo of a cleaner holding a mop in their right hand looked wrong in the RTL layout because it implied they were moving left, away from the user’s reading direction.
The practical solution is not to mirror images (never do this to photographs of people), but to source RTL-neutral imagery where possible — overhead shots, close-ups, symmetrical compositions. This is a design brief requirement now, not an afterthought.
The Testing Checklist
Before any Arabic release, we run through this checklist:
- Every icon with directional meaning has been tested in RTL and flips correctly
- No
letter-spacingis applied to any Arabic text - All text inputs have
dirauto-detection or are explicitly set - URLs, product names, and code snippets in Arabic prose are wrapped in
dir="ltr"inline spans - Numbers use
Intl.NumberFormatwith explicit locale - The layout is visually mirrored when
dir="rtl"is applied to<html>— elements that don’t mirror are bugs unless intentional - The
langattribute on<html>matches the displayed language — this affects spell check, hyphenation, and screen reader behavior -
hreflangalternates are present and correct for SEO
What RTL-First Actually Gets You
When you build RTL-first, a few things become true:
Your components are more composable. CSS logical properties and direction-agnostic layouts produce components that genuinely work in any context. They become easier to reuse because they carry fewer embedded assumptions.
Your codebase gets smaller. Bidirectional support that is baked in from the start requires no override stylesheets, no direction-conditional JavaScript, no [dir="rtl"] selector blocks. The base styles just work.
Your Arabic users notice. An interface built RTL-first feels different to an Arabic speaker than one adapted after the fact. The spacing is right, the rhythm is right, the typography breathes correctly. These are not things users articulate — they are things they feel as comfort or friction.
Two years in, every property we ship natively supports both directions from the first deployment. The cost of maintaining that is low. The cost of not having it is paid every time you open the Arabic version of a competitor’s product.
For the content strategy side of building for Arabic-speaking markets, see The Arabic Content Gap: Why Businesses Are Losing the MENA Market and How Arabic SEO Actually Works in 2026.