Hook
Our first Arabic PDF looked perfect in the browser previews. Then we opened it in Acrobat: boxes instead of letters, text running left-to-right, and header columns reversed. It took us 3 days to understand why — and about 47 lines of code to fix it. This is what we learned.
Context
Complyance generates compliance documents: technical reports, gap analysis summaries, risk assessments. Our users are in the EU, UAE, and US. The UAE market requires Arabic. Arabic is RTL. The documents must be legally credible — a broken layout or garbled text is not acceptable.
We use @react-pdf/renderer because it lets us write PDF templates as React components, which fits our stack. But Arabic RTL exposed a set of problems that took us several days to resolve.
The problem in detail
Three distinct issues, all interacting:
1. Font rendering. react-pdf uses PDFKit under the hood. Arabic requires a font that includes Arabic glyphs with proper ligature support. The default fonts don't have it. Loading the wrong font produces boxes (☐☐☐☐) or mangled character sequences.
2. Text direction. Arabic is right-to-left but react-pdf doesn't have a native RTL mode. CSS direction: rtl doesn't apply here — this is a layout engine, not a browser.
3. Layout mirroring. In an RTL document, the entire layout flips. Header alignment, column order, margin sides, icon placement — everything that's left-right in LTR becomes right-left in RTL. If you don't mirror the layout, the text renders RTL but the structure looks wrong.
Naive approach / what didn't work
First attempt: set textAlign: "right" on text elements and call it done.
<Text style={{ textAlign: "right" }}>{arabicText}</Text>
Result: text was right-aligned but character order was wrong. Arabic characters need bidirectional text shaping — each character's visual form depends on its neighbors. textAlign is a visual property; it doesn't handle bidirectional rendering.
Second attempt: found a mention of direction in the PDFKit docs. Tried to pass it through react-pdf's style prop. It was silently ignored — react-pdf doesn't pass unknown style properties to the underlying engine.
Actual solution
Step 1: Load an Arabic font
// src/lib/pdf-fonts.ts
import path from "path";
import { Font } from "@react-pdf/renderer";
Font.register({
family: "NotoSansArabic",
fonts: [
{
src: path.join(process.cwd(), "public/fonts/NotoSansArabic-Regular.ttf"),
fontWeight: "normal",
},
{
src: path.join(process.cwd(), "public/fonts/NotoSansArabic-Bold.ttf"),
fontWeight: "bold",
},
],
});
Noto Sans Arabic is the reliable choice — complete glyph coverage, open license, proper ligature support. Download both weights. Register before rendering any document.
Step 2: Locale-aware font selection in components
function getDocumentFont(locale: string): string {
return locale === "ar" ? "NotoSansArabic" : "Inter";
}
// In the document template:
const font = getDocumentFont(locale);
<Text style={{ fontFamily: font, fontSize: 12 }}>
{content}
</Text>
Step 3: Layout mirroring via conditional styles
react-pdf doesn't support RTL natively, so we built a small utility that flips layout props:
function rtl(locale: string) {
return locale === "ar";
}
function directedStyle(locale: string, ltrStyle: Style, rtlStyle: Style): Style {
return rtl(locale) ? rtlStyle : ltrStyle;
}
// Usage in template:
<View
style={{
flexDirection: directedStyle(locale,
{ flexDirection: "row" },
{ flexDirection: "row-reverse" }
).flexDirection,
textAlign: rtl(locale) ? "right" : "left",
}}
>
For most layout components, we pass locale as a prop and derive direction inline. It's verbose but explicit — you can see exactly what changes for RTL.
Step 4: Handling bidirectional text with unicode markers
For text content that mixes Arabic and Latin characters (product names, URLs, numbers), we inject Unicode bidirectional markers:
const RTL_MARK = "\u200F"; // RIGHT-TO-LEFT MARK
const LTR_MARK = "\u200E"; // LEFT-TO-RIGHT MARK
function wrapForLocale(text: string, locale: string): string {
if (locale !== "ar") return text;
return `${RTL_MARK}${text}${RTL_MARK}`;
}
This tells PDFKit's text engine to treat the enclosed text as RTL, which triggers proper bidirectional algorithm handling.
Step 5: The page itself
<Page
size="A4"
style={{
fontFamily: font,
// react-pdf doesn't have a page-level direction prop,
// so all RTL is handled via component-level styles above
}}
>
The full Arabic PDF template conditionally applies all the above. It's about 40 more lines than the English version — mostly the directedStyle calls and the font family threading.
What we learned
Font is the first problem. If you don't have a valid Arabic font registered, nothing else matters. Test font rendering with a simple "hello world" in Arabic before building the layout.
react-pdf has no native RTL. Don't look for a
directionprop or an RTL mode. It doesn't exist. You handle it manually throughflexDirection: "row-reverse",textAlign: "right", and unicode markers.flexDirection: "row-reverse"is your main tool. Most RTL layout issues come down to element order. Reversing flex direction handles headers, icon+text pairs, and multi-column layouts cleanly.Numbers and URLs are always LTR. Even in an Arabic document, version numbers, URLs, and code snippets should render left-to-right. Wrap them in
LTR_MARKmarkers. Forgetting this looks wrong and confuses readers.Test with actual Arabic text, not lorem ipsum. Lorem ipsum transliterated into Arabic characters won't trigger the same rendering issues as real Arabic text with proper ligatures and bidirectional content.
Font file paths are relative to
process.cwd(), not the source file. This bit us in Railway deployment. Usepath.join(process.cwd(), "public/fonts/...")not__dirname-relative paths.
What's next
The current implementation handles A4 documents. We haven't tested with A3 or letter size in Arabic. We also haven't handled Farsi (another RTL language with a different character set) — that would require a separate font registration.
The bigger gap: we're generating PDFs server-side in Next.js, which means every render is a cold start for the font registration. Caching the registered fonts across requests would help with document generation latency.
Community questions
Has anyone built a react-pdf template with full RTL support without reverting to a separate RTL-specific template file? We ended up with conditional styles throughout — curious if there's a cleaner abstraction.
What font do you use for Arabic PDF generation? Noto works but it's large. Are there lighter alternatives with comparable coverage?
For teams supporting multiple RTL languages (Arabic, Hebrew, Farsi) — do you maintain one template with locale conditions or separate templates per language family?
Top comments (0)