By Shehab Waleed, Founder, Cairo Studio 3 min read
Most text reveals on the web start the same way: split the line into words or letters, wrap each in an inline-block span, and stagger them. We use that pattern across our site. When we built the Arabic version, it broke in two separate ways, and only one of them involved Arabic.
Arabic letters depend on their neighbours
Most Arabic letters have different forms for the start, middle and end of a word, and the browser picks one by looking at what sits next to it. Wrap each letter in its own inline-block and the browser shapes it alone.
Whole: مدينة
Split: م د ي ن ة
You can measure the damage. In Chromium at 40px, that word is 70.5px wide when whole and 93.6px wide when split by letter, because each letter falls back to its wider isolated form. Splitting by word keeps the joins, since Arabic only joins inside a word.
Letter spacing breaks the joins as well, and tracking tends to live in shared classes where nobody thinks about Arabic. We reset it at the top.
[lang='ar'] [class*='tracking-'] {
letter-spacing: normal;
}So we stopped splitting it
Our text components ask the same question first.
const ARABIC =
/[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/;
export function hasArabic(value) {
return !!value && ARABIC.test(value);
}
If the answer is yes, the whole line animates as one unit. Word splitting would survive, but one check and no splitting is a simpler rule, and it removes any question about word order.
let targets;
if (hasArabic(value)) {
targets = [inner];
} else {
split = new SplitText(inner, { type: 'words' });
targets = split.words;
}
The bug that hit the English
Arabic pages still carry Latin text: service codes like BR and UX, numbers, project names. Under dir="rtl" these came out wrong. An inline-block counts as a neutral object in the bidi algorithm, so a row of them gets laid out from right to left, even when each one holds a Latin letter. Text that was never split gets reordered too, because nothing isolates it from the page direction. These are from our audit in headless Chrome.
- BR became RB.
- UX became XU.
- 32 Real Estate became Real Estate 32.
- [S: 01] became 01] [S:
The fix for a splitter is one attribute on the element being split. In a bare test, the same two spans read RB without it and BR with it.
<span dir={hasArabic(text) ? 'rtl' : 'ltr'}>{text}</span>When a component needs left to right geometry, like a horizontal track, set dir="ltr" on the track and restore the direction on each text panel inside it. Putting dir="ltr" on a whole section leaks into its headings and into every logical utility inside.
<div dir="ltr" class="track">
<div dir={isRtl ? 'rtl' : 'ltr'}>...</div>
</div>
Arabic type needs more room
Thmanyah, our Arabic display face, measures 1.434em from top to bottom of its ink. Youth, our Latin header face, measures 1.243em. A line height of 0.85 that looks sharp in English will clip Arabic, which needs at least 1.35 for display and 1.7 for body text.
Font order matters too. Thmanyah also covers Latin, from a serif half, so it has to come after the Latin face or your codes and numbers turn into serifs.
--font-header: 'Youth', 'Thmanyah Display', sans-serif;
None of this replaces writing the Arabic properly in the first place. It only makes sure the browser does not undo that work.
