Arabic Typography for Product Interfaces: Font Choices That Actually Render Well
A practical guide to choosing Arabic web fonts and tuning line-height, OpenType shaping and font loading so Arabic type renders the way it's supposed to — not just the way it compiles.
Most localization bugs get caught because something looks obviously broken: a mirrored icon, a layout that overflows, a date format nobody expected. Bad Arabic typography doesn't announce itself that way. The page loads, the text is the right language, direction is correct, and it still reads as slightly wrong to anyone who grew up reading Arabic — cramped lines, letters that look disconnected when they shouldn't, a weight that was never designed for small UI text. The bug is real, but it lives in decisions made months earlier: which font got picked, what line-height it shipped with, whether anyone thought about a script where most letters change shape depending on their neighbors, in a way Latin letters never do.
This is a guide to those decisions, specifically for product and frontend teams shipping an Arabic interface: which fonts are actually documented and maintained, what Arabic script does differently at the rendering level, and the concrete CSS and font-loading choices that follow from it. It assumes you already have direction and layout handled — if you don't, that's a separate problem covered in our companion piece on building a design system for LTR and RTL.
Key Takeaways
- Arabic is a cursive, contextual script: most letters take different joined forms depending on their neighbors, and per the W3C's Arabic orthography notes, "glyph shapes are highly variable for Arabic letters" — the font, not your CSS, is doing most of that work.
- Arabic has no letter case. Anything in your design system that assumes an uppercase/lowercase toggle (small caps, all-caps buttons,
text-transform: uppercase) does nothing meaningful to Arabic text and should be scoped to Latin only.- The core joining behavior — initial, medial, final and isolated letterforms — is applied automatically by the browser's text-shaping engine once you declare
lang="ar", not something you turn on withfont-feature-settings. Typotheque's OpenType-in-CSS guide states plainly that "all features required for Arabic are activated automatically if the language is declared."- Arabic ascenders, descenders and diacritics extend further from the baseline than Latin's do, so lines need more vertical room. The W3C's own guidance is qualitative, not a fixed multiplier — "the line height needs to be significantly higher than for Latin script text" — and no single number is authoritative enough to hard-code.
- Google Fonts hosts several actively maintained Arabic families with genuinely different design briefs — Noto Sans Arabic, Cairo, Almarai and IBM Plex Sans Arabic — but only some of them (Noto Sans Arabic and Cairo, as of this writing) ship as true variable fonts; check per family rather than assuming.
- Arabic's glyph set is large enough (Noto Sans Arabic alone covers over a thousand characters across six Unicode blocks) that the same
unicode-rangesubsetting and WOFF2 practices web.dev recommends for CJK fonts apply here too.
What Arabic script does that your font-loading strategy needs to account for
Three structural facts about Arabic explain almost every typography decision that follows, and none of them are opinions — they're documented properties of the script itself.
It's cursive by default, not by font choice. In Latin type, letters are static shapes placed next to each other. In Arabic, per the W3C's Arabic and Persian Layout Requirements and its companion orthography notes, "letters in a word are joined up. Fonts need to produce the appropriate joining form for a letter, according to its visual context." Most Arabic letters have up to four shapes — isolated, initial, medial and final — and a handful join only on one side. This is why you can't treat an Arabic string like a Latin one with different glyphs substituted in: the actual shape of every letter depends on what's next to it, and that substitution is the font's shaping tables doing their job, not something you can approximate with letter-spacing or manual glyph selection.
There's no case, and no case-adjacent effects. Arabic orthography has never had an uppercase/lowercase distinction. That sounds like a trivial fact until you check what depends on case in a typical design system: text-transform: uppercase on buttons, small-caps for abbreviations, "sentence case" style guides for headings. None of those concepts exist in Arabic, so a button component that forces uppercase needs a locale-aware exception, not a font that "supports" uppercase Arabic — no font can support a distinction the script doesn't have.
Diacritics and vowel marks push text further from the baseline than Latin's ascenders and descenders do. Modern UI copy is usually unvocalized (no diacritics), but Quranic text, some educational content, and disambiguation of homographs still use them, and even unvocalized Arabic has letters like ل (lam) and ج (jeem) whose ascenders and descenders travel further than a Latin l or p typically does. The W3C's orthography reference is explicit that "ascenders and descenders of Arabic letters tend to travel further from the baseline than is usual in Latin script text," and that when diacritics are present, "the line height needs to be significantly higher than for Latin script text." That's a real, sourced claim — what it deliberately isn't is a specific ratio, and we come back to why that matters below.
Choosing a typeface: what's actually documented, not just what's popular
"Use Noto Sans Arabic, it's the safe default" is common advice, and it's not wrong, but it skips the fact that Google Fonts now hosts several actively maintained Arabic families with genuinely different design briefs. Here's what's documented about the ones product teams reach for most.
Noto Sans Arabic is part of Google's Noto project, whose stated goal is a font family with no "tofu" — the empty boxes that show up when a font lacks a glyph — across the world's scripts. Per Noto's own font documentation, the current Noto Sans Arabic ships as a variable font with wght and wdth axes, covers roughly 1,600 glyphs mapping to over a thousand characters across six Unicode blocks, and exposes about a dozen OpenType features. It's the closest thing to a neutral, maximally-compatible default, which is exactly why it's a reasonable starting point and rarely the most distinctive choice for a brand.
Cairo, per its description in the Google Fonts repository, was designed by Mohamed Gaber as an extension of the Latin typeface Titillium Web into Arabic, drawing on Kufi calligraphic style, with "wide open counters and short ascenders and descenders that minimize length while maintaining easy readability." It ships in eight static weights plus a variable font, and its Arabic glyph set covers Arabic, Farsi and Urdu — worth checking explicitly if your product serves Persian or Urdu-script users alongside Arabic ones, since not every "Arabic" font covers those extended letterforms.
Almarai, per its own Google Fonts repository description, was designed by Boutros™ specifically "for optimal readability and suitability for both online and offline applications," engineered with "low contrast and wide aperture" across its four weights "to enhance the readability of the font in various media, especially on screen." That screen-legibility brief, stated directly by the foundry, is why Almarai shows up disproportionately often in MENA product UI rather than editorial or print work.
IBM Plex Sans Arabic is the Arabic extension of IBM's open-source Plex family, designed by Wael Morcos and Khajag Apelian of Morcos Key, whose work on the project won a D&AD Pencil. IBM's own framing of Plex, per the Google Fonts repository description, calls it a "neutral, yet friendly Grotesque style typeface" designed around "the unique relationship between mankind and machine" — a brand-driven brief rather than a screen-legibility one, which shows in its slightly higher contrast compared with Almarai.
None of this makes one of these fonts objectively "correct." It means the decision should be made the way you'd make it for a Latin typeface: read what the type designer actually says the font is for, check whether it's genuinely variable or only offers a couple of static weights, and check language coverage if you serve more than Arabic proper.
Line-height: real guidance, without a fabricated number
Search "line-height for Arabic" and you'll find plenty of blog posts asserting a specific multiplier — 1.5, 1.7, 1.8 — as if it were a settled constant. None of those numbers trace back to a specification or a font foundry; they're informal rules of thumb repeated between blog posts. What's actually documented is qualitative: Arabic letterforms and diacritics extend further from the baseline than Latin's do, and line-height needs to increase to accommodate that — an ascender/descender relationship, not a fixed ratio, because it depends on the specific font's metrics, not on Arabic as a script in the abstract.
The practical implication is to set line-height as a unitless value relative to font size, exactly as you would for any script, rather than hard-coding pixels:
[lang="ar"] {
/* Unitless line-height scales with font-size and lets the browser
use the loaded font's own vertical metrics, instead of assuming
Latin proportions carried over from a shared base stylesheet.
Set --arabic-line-height empirically per typeface — by rendering
real Arabic body copy at production font sizes and checking that
diacritics and descenders don't collide between lines — rather
than copying a number from a blog post, this one included. */
line-height: var(--arabic-line-height);
}
There is no shortcut around measuring this against your actual chosen font: render real Arabic body copy at your real font sizes, including the longest words and any diacritics your content uses, and increase line-height until nothing from one line touches the line above or below it. The one thing you should not do is inherit a Latin line-height value unmodified for Arabic text sharing a layout, and the one case that needs deliberate handling is a single line mixing scripts (an Arabic sentence with an English brand name, or a price in Latin digits): per the W3C's orthography notes, when scripts share a line the line-height has to accommodate whichever script demands more vertical space, which in practice is almost always Arabic.
font-feature-settings: less relevant to Arabic correctness than you'd guess
It's a reasonable assumption that shipping correct Arabic means reaching for font-feature-settings the way you might for Latin ligatures or tabular figures. For the features that actually make Arabic joined text look right, that assumption is backwards.
The four positional features that produce joined letterforms — isol, init, medi, fina — along with required ligatures (rlig) and contextual alternates (calt), are documented in the OpenType Arabic shaping reference as features the text-shaping engine applies in a fixed, mandatory order based on each letter's computed joining state — not features an author toggles. Practically, that means the browser's shaping engine (HarfBuzz, on most platforms and browsers) already applies them correctly as long as it knows the text is Arabic, which is exactly what Typotheque's guide to OpenType features in CSS means when it says "all features required for Arabic are activated automatically if the language is declared":
<!-- This does more work than it looks like it does. Declaring lang
tells the shaping engine which script rules to apply; dir is a
separate, complementary signal about layout direction. -->
<p dir="rtl" lang="ar">
نص حكيم له سر قاطع وذو شأن عظيم مكتوب على ثوب أخضر ومغلف بجلد أزرق
</p>
Skipping lang="ar" on Arabic content — a genuinely common bug when Arabic strings get interpolated into a component tree that only sets lang at the document root — doesn't just hurt accessibility and search indexing. It can affect which shaping rules a browser applies to ambiguous cases.
What font-feature-settings (or, preferably, the higher-level font-variant-* properties) is still legitimately for in Arabic UI is the same category of optional typographic choices it handles in Latin text: discretionary ligatures, stylistic sets a font exposes for calligraphic variation, or tabular figures for aligning numbers in a table. MDN's own guidance is to prefer font-variant-* properties where a browser supports them, and treat font-feature-settings as the low-level fallback:
/* Prefer the standard property when supported */
.data-table td {
font-variant-numeric: tabular-nums;
}
/* Fall back to font-feature-settings only where needed */
@supports not (font-variant-numeric: tabular-nums) {
.data-table td {
font-feature-settings: "tnum";
}
}
Variable fonts: fewer requests, and a weight range that actually fits UI
Static Arabic web fonts used to mean shipping several separate files — regular, medium, bold, and so on — each carrying the full set of joined letterforms Arabic requires, which is a larger glyph set per file than an equivalent Latin weight. Variable fonts change that math directly. MDN's variable fonts guide describes them as packing "many different variations of a typeface... into a single file," typically landing close to the size of four static files combined while exposing the entire weight and width range rather than four fixed stops.
Noto Sans Arabic and Cairo are both documented as shipping genuinely variable versions with a wght axis (Noto Sans Arabic additionally exposes wdth), which means one @font-face declaration replaces what used to be a whole weight family — worth checking explicitly, since not every "Arabic web font" listed on Google Fonts has caught up: IBM Plex Sans Arabic, for instance, is still distributed as seven separate static weights rather than a single variable file. Cairo's variable instance exposes a wght axis spanning 200 to 900 — its full documented static-weight range collapsed into one file:
@font-face {
font-family: "Cairo Variable";
src: url("/fonts/cairo-variable.woff2") format("woff2-variations");
font-weight: 200 900; /* Cairo's documented wght axis range */
font-display: swap;
}
h1 {
font-family: "Cairo Variable", sans-serif;
font-weight: 700; /* picks a point inside the range, no separate file */
}
This matters more for Arabic UI than it might for Latin, precisely because a heavier glyph set makes each additional static weight a proportionally bigger download — the same reason subsetting, covered next, pays off faster for Arabic than for a small Latin character set.
Loading a large glyph set without hurting performance
Arabic's glyph count is the real performance lever most teams overlook. Noto Sans Arabic alone spans roughly 1,600 glyphs across more than a thousand characters, and that's before accounting for extended Arabic-Indic digits, Quranic annotation marks, or Persian/Urdu extensions some "Arabic" fonts bundle in. web.dev's guide to reducing web font size makes the general case that "unicode-range subsetting is particularly important for [scripts] where the number of glyphs is much larger than in Western languages" — and Arabic, alongside CJK, is squarely the case that guidance is written for.
Three practical moves follow directly from that:
- Subset to what you actually render. If your UI never displays Quranic diacritics or Persian-only letters, don't ship the glyphs for them. Tools like
pyftsubset(part of fonttools) or Google Fonts' owntext=API parameter can generate a subset covering only the Arabic block plus whatever Latin characters your interface mixes in. - Use
unicode-rangeto split scripts into separate@font-faceblocks. This lets the browser skip downloading the Arabic subset entirely for a page rendered in English, and vice versa:
@font-face {
font-family: "UI Sans";
src: url("/fonts/ui-sans-latin.woff2") format("woff2");
unicode-range: U+0000-00FF;
}
@font-face {
font-family: "UI Sans";
src: url("/fonts/ui-sans-arabic.woff2") format("woff2");
unicode-range: U+0600-06FF, U+0750-077F, U+FB50-FDFF, U+FE70-FEFF;
}
- Serve WOFF2 only, and choose
font-displaydeliberately. web.dev notes WOFF2's Brotli-based compression beats WOFF by up to 30%, and with a large Arabic glyph set that compression difference compounds. Forfont-display, web.dev's font best-practices guide frames the choice as a trade-off:swapkeeps text visible immediately in a fallback font at the risk of a layout shift when the real font swaps in, whileoptionalavoids that shift entirely by accepting the fallback font if the real one isn't ready fast enough. For Arabic specifically, don't assume your fallback stack behaves the way it does for Latin: if the fonts listed before your Arabic webfont in thefont-familystack don't include Arabic glyphs, the browser will substitute a completely different system font for the Arabic characters — with its own weight, proportions and joining quality — while your webfont loads, rather than showing a metrics-close approximation of the same design the way a Latin fallback often can. Test the actual fallback your target browsers and operating systems pick, rather than assumingswapdegrades as gracefully for Arabic as it does for Latin body text.
Putting it together
None of this replaces the layout and direction work covered in our design-system piece on LTR and RTL — it assumes that groundwork is already in place. What it adds is the layer above layout: picking a font whose documented design brief actually matches your product (screen-legible UI copy versus brand-driven display type), giving it the vertical room its script requires instead of a Latin default, declaring lang="ar" so the browser's shaping engine does the joining work it's already built to do, and loading a large glyph set the same way any large glyph set should be loaded — subsetted, compressed, and split by script. Get the font right and the rest of your Arabic interface has a foundation that doesn't quietly work against it.