Arabic text looks simple until you try to render it in a browser. Then it reveals a dozen ways to go wrong.
The first decision was the font. The Quran's Uthmani script uses specific ligatures and harakat (diacritical vowel marks — the small lines, circles, and symbols above and below letters that indicate pronunciation) that not every Arabic font supports properly. The default system Arabic fonts — Geeza Pro on macOS/iOS, Arabic Typesetting on Windows — are serviceable but inconsistent across platforms.
I chose Amiri, a free typeface by Khaled Hosny, specifically designed for Arabic and Quranic text. It's modeled after classical Naskh style, has full harakat support, and is available on Google Fonts. Loading it is straightforward:
// layout.tsx
const amiri = Amiri({
weight: ["400", "700"],
subsets: ["arabic"],
variable: "--font-amiri",
});Then in CSS, any element displaying Quranic Arabic gets font-family: var(--font-amiri).
The RTL layout was the next thing to get right. Arabic is written right-to-left. In a Next.js app, the default text direction is ltr. If you just render Arabic text without specifying direction, it will still display — Arabic Unicode characters are bidirectional by default in most browsers — but the text alignment will be wrong, and any punctuation or numbers mixed into the Arabic will appear on the wrong side.
The fix is dir="rtl" on the Arabic text container:
<p dir="rtl" lang="ar" className="font-amiri text-right leading-loose">
{ayah.arabicText}
</p>The lang="ar" attribute is important for screen readers and browser text rendering — it tells the browser this content is Arabic and enables language-specific hyphenation and glyph selection.
The API structure is worth understanding. Al-Quran Cloud's endpoint for a single Surah with multiple editions is:
GET /v1/surah/{number}/editions/{edition1},{edition2},{edition3}For Tilawa I use:
/v1/surah/{id}/editions/quran-uthmani,en.sahih,bn.bengaliThis returns a JSON array with three items — one per edition — each containing all Ayahs for that Surah. I merge them client-side into a unified Ayah array where each Ayah object has arabicText, englishText, and banglaText.
One API quirk: the Surah list endpoint (/v1/surah) returns only the default edition metadata. To display the Arabic name of each Surah on the list page, I use the name field from the list response, which is the full Arabic name with definite article and harakat. The englishName and englishNameTranslation give the romanized name and its English meaning.
The font size sliders were my solution to a problem I noticed while testing: my mother needed larger Arabic text to read comfortably. My developer instinct said CSS font-size and a range slider. The slider updates a CSS custom property on the parent element, and the Arabic and translation text sizes are independent:
const [arabicFontSize, setArabicFontSize] = useState(28); // px const [transFontSize, setTransFontSize] = useState(15); // px
<p style={{ fontSize: `${arabicFontSize}px` }} dir="rtl" lang="ar">
{ayah.arabicText}
</p>The settings are not persisted to localStorage yet — they reset on page reload. That's a known gap. When the user returns to the same Surah, they'd need to readjust. It's next on the list.
