Generating PDFs with React-PDF
CRITICAL REQUIREMENTS
- Fonts MUST be local files - Remote font URLs (http/https) do NOT work. Always download fonts to local files before using them.
- Wrap async code in IIFE - Top-level await causes errors. Always use
(async () => { ... })()pattern. - Disable hyphenation for custom fonts - Custom fonts lack hyphenation dictionaries and may
crash or break words incorrectly. Always call
Font.registerHyphenationCallback((word) => [word]);after registering custom fonts.
Files
references/google-fonts.txt- Metadata for ~65 popular Google Fonts with TrueType URLs. Each line is a font variant in tab-separated format:font name,style,category,weight,url.references/components.md- Full component API reference and supported CSS propertiesassets/example-template.tsx- Minimal working example demonstrating fixed footers, page numbers, and unbreakable content. Read this before starting to understand the basic patterns. Note: not all APIs are shown here — always refer to the docs andreferences/components.mdfor the full API.
Prerequisites
npm install react @react-pdf/renderer
npm install -D tsx @types/react
tsx runs TypeScript + JSX files directly via Node with no config — no tsconfig.json needed. It
uses esbuild under the hood and handles JSX transformation automatically.
Core Components
- Document: Root component (metadata, settings)
- Page: Individual pages (A4, Letter, or custom dimensions)
- View: Container component (similar to div)
- Text: Text content, supports nesting for inline styling
- Image: Embed images (JPG, PNG, base64)
- Link: Clickable hyperlinks (external or internal)
- Note: Annotation notes
- Canvas: Freeform drawing with pdfkit methods
- Svg: Vector graphics (Circle, Rect, Path, Line, Polygon, etc.)
- StyleSheet: Create reusable styles
For full component props and CSS properties, see references/components.md.
Basic Example
import React from "react";
import { Document, Page, Text, View, StyleSheet, renderToFile } from "@react-pdf/renderer";
const styles = StyleSheet.create({
page: { flexDirection: "column", backgroundColor: "#ffffff", padding: 40 },
title: { fontSize: 24, marginBottom: 20, fontWeight: "bold" },
text: { fontSize: 12, lineHeight: 1.5 },
});
const MyDocument = () => (
<Document>
<Page size="A4" style={styles.page}>
<View style={{ margin: 10, padding: 20 }}>
<Text style={styles.title}>Document Title</Text>
<Text style={styles.text}>Your content here</Text>
</View>
</Page>
</Document>
);
(async () => {
await renderToFile(<MyDocument />, "./output.pdf");
console.log("PDF saved!");
})();
Running Scripts
PDF generation scripts use JSX, which Node cannot run directly. Use tsx to execute them:
npx tsx my-document.tsx
npx tsx works without installing tsx globally — it downloads on demand. If tsx is installed as a
dev dependency (npm install -D tsx), it runs instantly without the npx download step.
Always wrap rendering in async IIFE:
// Good
(async () => {
await renderToFile(<MyDocument />, "./output.pdf");
})();
// Bad - top-level await may fail
await renderToFile(<MyDocument />, "./output.pdf");
Previewing PDFs
To visually inspect generated PDFs, convert pages to images. Try pdftoppm first (often
pre-installed), fall back to Python's PyMuPDF if unavailable.
Option 1: pdftoppm (poppler-utils) — preferred, no install needed in many environments:
pdftoppm -png -r 200 document.pdf preview
# -> preview-1.png, preview-2.png, ...
Option 2: PyMuPDF (Python) — fallback if pdftoppm is not available:
pip install pymupdf
import fitz
doc = fitz.open("document.pdf")
for i, page in enumerate(doc):
pix = page.get_pixmap(dpi=200)
pix.save(f"page-{i+1}.png")
Rendering Methods
import { renderToFile, renderToBuffer } from "@react-pdf/renderer";
// To file
(async () => {
await renderToFile(<MyDocument />, "./document.pdf");
})();
// To buffer
(async () => {
const buffer = await renderToBuffer(<MyDocument />);
})();
Styling
Three methods: StyleSheet.create(), inline objects, or mixed arrays.
const styles = StyleSheet.create({ container: { padding: 20 } });
<View style={styles.container} />
<View style={{ padding: 20 }} />
<View style={[styles.container, { marginTop: 10 }]} />
Supported Units
pt (default, 72 DPI), in, mm, cm, %, vw, vh
Common Style Properties
{
// Flexbox
flexDirection: "row", justifyContent: "space-between", alignItems: "center",
flexWrap: "wrap", gap: 10,
// Box model
margin: 10, padding: 20, width: "100%", height: 200,
// Borders
borderWidth: 1, borderColor: "#333", borderRadius: 5, borderStyle: "solid",
// Colors
backgroundColor: "#f0f0f0", color: "#000", opacity: 0.8,
// Typography
fontSize: 12, fontWeight: "bold", fontFamily: "Helvetica", fontStyle: "italic",
lineHeight: 1.5, textAlign: "center", textDecoration: "underline",
textTransform: "uppercase", letterSpacing: 1,
// Position
position: "absolute", top: 0, left: 0, right: 0, bottom: 0, zIndex: 10,
// Transforms
transform: "rotate(45deg)", transformOrigin: "center",
}
Images
Local files are most reliable. Remote URLs may fail due to network/CORS issues.
import { Image } from '@react-pdf/renderer';
<Image src="./images/photo.jpg" style={{ width: 200, height: 150 }} />
<Image src={{ data: buffer, format: 'png' }} />
SVG files cannot be used as Image sources. Read the SVG source and recreate using react-pdf Svg components.
SVG Graphics
import { Svg, Circle, Rect, Path, Line, G, Defs, LinearGradient, Stop } from "@react-pdf/renderer";
<Svg width="200" height="200" viewBox="0 0 200 200">
<Defs>
<LinearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
<Stop offset="0%" stopColor="#3498db" />
<Stop offset="100%" stopColor="#9b59b6" />
</LinearGradient>
</Defs>
<Circle cx="100" cy="100" r="50" fill="url(#grad1)" />
<Rect x="10" y="10" width="50" height="50" fill="#e74c3c" />
<Path d="M10,50 Q50,10 90,50" stroke="#2ecc71" strokeWidth="2" fill="none" />
</Svg>;
Data Visualization with SVG Charts
Bar Chart
const BarChart = ({ data, width = 400, height = 200 }) => {
const maxVal = Math.max(...data.map((d) => d.value));
const barWidth = (width - 60) / data.length - 8;
return (
<Svg width={width} height={height + 30} viewBox={`0 0 ${width} ${height + 30}`}>
{/* Y-axis */}
<Line x1="50" y1="10" x2="50" y2={height} stroke="#ccc" strokeWidth="1" />
{/* X-axis */}
<Line x1="50" y1={height} x2={width} y2={height} stroke="#ccc" strokeWidth="1" />
{data.map((d, i) => {
const barHeight = (d.value / maxVal) * (height - 20);
const x = 60 + i * (barWidth + 8);
const y = height - barHeight;
return (
<G key={i}>
<Defs>
<LinearGradient id={`bar-${i}`} x1="0%" y1="0%" x2="0%" y2="100%">
<Stop offset="0%" stopColor={d.color || "#3498db"} />
<Stop offset="100%" stopColor={d.color ? d.color + "88" : "#2980b9"} />
</LinearGradient>
</Defs>
<Rect x={x} y={y} width={barWidth} height={barHeight} fill={`url(#bar-${i})`} rx="3" />
</G>
);
})}
</Svg>
);
};
Pie Chart
const PieChart = ({ data, size = 200 }) => {
const total = data.reduce((sum, d) => sum + d.value, 0);
const cx = size / 2, cy = size / 2, r = size / 2 - 10;
let currentAngle = -Math.PI / 2;
return (
<Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
{data.map((d, i) => {
const angle = (d.value / total) * 2 * Math.PI;
const x1 = cx + r * Math.cos(currentAngle);
const y1 = cy + r * Math.sin(currentAngle);
const x2 = cx + r * Math.cos(currentAngle + angle);
const y2 = cy + r * Math.sin(currentAngle + angle);
const largeArc = angle > Math.PI ? 1 : 0;
const path = `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2} Z`;
currentAngle += angle;
return <Path key={i} d={path} fill={d.color} />;
})}
</Svg>
);
};
Line Chart
const LineChart = ({ data, width = 400, height = 200 }) => {
const maxVal = Math.max(...data.map((d) => d.value));
const minVal = Math.min(...data.map((d) => d.value));
const range = maxVal - minVal || 1;
const stepX = (width - 70) / (data.length - 1);
const points = data.map((d, i) => ({
x: 60 + i * stepX,
y: 15 + (1 - (d.value - minVal) / range) * (height - 30),
}));
const linePath = points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
const areaPath = `${linePath} L ${points[points.length - 1].x} ${height} L ${points[0].x} ${height} Z`;
return (
<Svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}>
<Defs>
<LinearGradient id="area-fill" x1="0%" y1="0%" x2="0%" y2="100%">
<Stop offset="0%" stopColor="#3498db" stopOpacity="0.3" />
<Stop offset="100%" stopColor="#3498db" stopOpacity="0.05" />
</LinearGradient>
</Defs>
<Path d={areaPath} fill="url(#area-fill)" />
<Path d={linePath} stroke="#3498db" strokeWidth="2" fill="none" />
{points.map((p, i) => (
<Circle key={i} cx={p.x} cy={p.y} r="3" fill="#3498db" />
))}
</Svg>
);
};
Using Icons
Read SVG source from icon libraries and convert to react-pdf Svg components:
npm install lucide-static
import { Svg, Path, Rect } from "@react-pdf/renderer";
// Converted from lucide-static/icons/mail.svg
const MailIcon = ({ size = 12, color = "#888" }) => (
<Svg width={size} height={size} viewBox="0 0 24 24">
<Path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" stroke={color} strokeWidth={2} fill="none" />
<Rect x="2" y="4" width="20" height="16" rx="2" stroke={color} strokeWidth={2} fill="none" />
</Svg>
);
Links and Navigation
<Link src="https://example.com"><Text>Visit website</Text></Link>
<View id="section-1"><Text>Target</Text></View>
<Link src="#section-1"><Text>Jump to Section 1</Text></Link>
Dynamic Content and Page Numbers
<Text render={({ pageNumber, totalPages }) => `Page ${pageNumber} of ${totalPages}`} />
Fixed Headers/Footers
<Page size="A4">
<View fixed style={{ position: "absolute", top: 20, left: 30, right: 30 }}>
<Text>Header</Text>
</View>
<View style={{ marginTop: 60, marginBottom: 60 }}>
<Text>Content</Text>
</View>
<Text
fixed
style={{ position: "absolute", bottom: 20, left: 30, right: 30, textAlign: "center" }}
render={({ pageNumber, totalPages }) => `Page ${pageNumber} of ${totalPages}`}
/>
</Page>
Page Breaks and Wrapping
<View break /> // Force page break
<View wrap={false}><Text>Keep together</Text></View> // Prevent breaking inside
<Text orphans={2} widows={2}>Long text...</Text> // Orphan/widow control
<View minPresenceAhead={100}><Text>Content</Text></View> // Min space before break
Custom Fonts
CRITICAL: All font sources MUST be local file paths. Remote URLs do not work.
import { Font } from "@react-pdf/renderer";
Font.register({
family: "Roboto",
fonts: [
{ src: "./fonts/Roboto-Regular.ttf", fontWeight: "normal" },
{ src: "./fonts/Roboto-Bold.ttf", fontWeight: "bold" },
{ src: "./fonts/Roboto-Italic.ttf", fontStyle: "italic" },
],
});
// Always disable hyphenation when using custom fonts
Font.registerHyphenationCallback((word) => [word]);
Built-in fonts: Courier, Helvetica, Times-Roman (each with Bold, Italic/Oblique variants)
Font weight values: thin (100), ultralight (200), light (300), normal (400), medium (500), semibold (600), bold (700), ultrabold (800), heavy (900)
Google Fonts
Use references/google-fonts.txt to find font URLs, then download locally:
# Find the font URL
grep "^Roboto" skills/react-pdf/references/google-fonts.txt | grep "700" | grep "normal"
# Download
mkdir -p fonts
curl -sL "<url-from-grep>" -o fonts/Roboto-Bold.ttf
# Verify - must show "TrueType Font data"
file fonts/Roboto-Bold.ttf
If file shows "HTML document" or "ASCII text", the download failed. Try a different URL or search
GitHub for the font's official repo with TTF files.
Emoji
Emoji won't render in PDFs unless you register an emoji source. Install twemoji-emojis to get
local Twemoji PNG assets — no internet needed at render time.
npm install twemoji-emojis
import { Font } from "@react-pdf/renderer";
Font.registerEmojiSource({
format: "png",
url: "node_modules/twemoji-emojis/vendor/72x72/",
});
Then use emoji directly in Text: <Text>Hello</Text>
Reporting & Data Templates
KPI Dashboard Card
const KPICard = ({ title, value, change, color = "#3498db" }) => (
<View style={{
width: "48%", padding: 15, marginBottom: 10,
borderRadius: 8, backgroundColor: "#fafbfc",
borderLeft: `4px solid ${color}`,
}}>
<Text style={{ fontSize: 9, color: "#888", textTransform: "uppercase", letterSpacing: 1 }}>
{title}
</Text>
<Text style={{ fontSize: 22, fontWeight: "bold", marginTop: 4, color: "#1a1a2e" }}>
{value}
</Text>
<Text style={{ fontSize: 9, color: change >= 0 ? "#27ae60" : "#e74c3c", marginTop: 4 }}>
{change >= 0 ? "+" : ""}{change}% vs last period
</Text>
</View>
);
Data Table
const DataTable = ({ headers, rows, stripeColor = "#f8f9fa" }) => (
<View style={{ marginVertical: 10 }}>
{/* Header Row */}
<View style={{
flexDirection: "row", backgroundColor: "#1a1a2e", borderRadius: "4 4 0 0",
padding: "8 12",
}}>
{headers.map((h, i) => (
<Text key={i} style={{
flex: h.flex || 1, fontSize: 9, fontWeight: "bold",
color: "#ffffff", textTransform: "uppercase", letterSpacing: 0.5,
}}>{h.label}</Text>
))}
</View>
{/* Data Rows */}
{rows.map((row, i) => (
<View key={i} style={{
flexDirection: "row", padding: "6 12",
backgroundColor: i % 2 === 0 ? "#fff" : stripeColor,
borderBottom: "0.5px solid #eee",
}}>
{headers.map((h, j) => (
<Text key={j} style={{
flex: h.flex || 1, fontSize: 9, color: "#333",
}}>{row[h.key]}</Text>
))}
</View>
))}
</View>
);
Progress Bar
const ProgressBar = ({ value, max = 100, color = "#3498db", width = "100%" }) => (
<View style={{ width, height: 8, backgroundColor: "#eee", borderRadius: 4, marginVertical: 4 }}>
<View style={{
width: `${(value / max) * 100}%`, height: 8,
backgroundColor: color, borderRadius: 4,
}} />
</View>
);
Other Features
// Canvas drawing
<Canvas style={{ width: 200, height: 200 }}
paint={(painter, w, h) => { painter.circle(w/2, h/2, 50).fill("#3498db"); }} />
// Annotation notes
<Note style={{ color: "yellow" }}>Annotation text</Note>
// Hyphenation
Font.registerHyphenationCallback((word) => [word]); // disable
// Debug mode - visualize boundaries
<View debug><Text debug>Debug text</Text></View>
// Document metadata
<Document title="My Doc" author="Author" subject="Report" language="en-US" pdfVersion="1.5" />
Best Practices
- Use
StyleSheet.create()— define styles once and reuse - Compress images before embedding, use
cache={true}for remote images - Test page breaks — content may flow differently than expected
- Prefer flexbox over absolute positioning
- Use
fixedprop for headers/footers on every page - Use
debug={true}to visualize element boundaries - Wrap rendering in try-catch blocks
- For data reports, create reusable chart components (BarChart, PieChart, LineChart)
- Use color palettes consistently across charts and KPI cards
- Always preview generated PDFs by converting to images for visual verification
Common Issues
Text overflow: <Text style={{ width: 200, maxLines: 3, textOverflow: "ellipsis" }}>...</Text>
Missing fonts: Download locally and register with local file paths. Remote URLs will NOT work.
Unexpected page breaks: Use wrap={false} to keep content together, or <View break /> to
force breaks.