Sunday, October 19, 2025
React PDF Viewers: The 2025 Guide

If you’re building a React app that needs to render PDFs, you’ve got a buffet of choices—from lightweight, OSS components to full‑blown commercial SDKs with redaction, signing, and real‑time collaboration. This guide breaks down the landscape, trade‑offs, and gives you drop‑in snippets to get started fast (including Next.js tips).
TL;DR
- Quickest embed:
<iframe>/<object>to browser viewer (zero JS, zero features). - Most popular OSS:
react-pdf(PDF.js under the hood; good defaults, easy). - Headless control: use
pdfjs-distdirectly for custom UI/virtualization. - Feature-heavy (annotations, forms, signing, redaction): commercial SDKs like Apryse WebViewer (formerly PDFTron), PSPDFKit, Foxit Web SDK.
- Mobile web: prefer canvas‑based renderers (PDF.js) + careful memory management.
- Next.js: dynamic import client components, use workers, disable SSR for the viewer.
The Options (at a glance)
Option License Strengths Gaps / Caveats Good for <iframe> / <object> / Google Viewer Free Fastest to integrate; no deps Limited features; browser-dependent UI; origin/CSP issues Internal tools, quick previews react-pdf (uses PDF.js) MIT Simple component API; text selection; forms (basic) Bundle size; pagination perf for huge PDFs; SSR quirks Product UIs with standard needs pdfjs-dist (raw PDF.js) Apache-2.0 Max control; tune rendering/virtualization Build your own UI; more plumbing Custom readers, infinite scroll Apryse WebViewer (PDFTron) Commercial Rich annotations, forms, redaction, collaboration, Office files Licensing cost; heavier bundle Enterprise workflows PSPDFKit for Web Commercial Best-in-class UX; signing, stamping, collaboration Licensing cost Document-heavy SaaS Foxit Web SDK Commercial Strong enterprise features; performance Licensing cost Enterprise integrations pdf-viewer-reactjs / react-pdf-viewer OSS (varies) Prebuilt toolbar/plugins, thumbnails Maturity varies; plugin ecosystems differ Faster OSS onboarding
Note: Libraries like pdf-lib or PDFKit are for creation/manipulation, not end‑user viewing.
How PDF Rendering Works (why it matters)
- PDF.js (canvas/SVG): Most OSS viewers rely on Mozilla’s PDF.js to parse and render each page to
<canvas>(or sometimes SVG). Canvas is typically faster and more compatible on mobile; SVG may have better selectable text but can bloat the DOM. - Virtualization: Rendering only visible pages prevents memory blowups—vital for 500+ page docs on mobile.
- Workers: Offload parsing to a Web Worker (
pdf.worker.js). Without it, the main thread janks. - Range Requests: Enable HTTP byte‑range so users can open page 300 without downloading everything.
1) The Simplest Path: Embed the Browser’s Viewer
<object data="/example.pdf#toolbar=0&zoom=page-width" type="application/pdf" width="100%" height="100%">
<iframe src="/example.pdf#toolbar=0&zoom=page-width" width="100%" height="100%"></iframe>
</object>
Pros: zero JS, browser handles zoom/print.
Cons: inconsistent UI across browsers; limited features; b locked by X-Frame-Options or CSP when using remote files.
Tip: For public files, you can use a linking fallback to the browser’s native viewer when JS fails.
2) react-pdf (most popular OSS)
Install
npm i react-pdf
Minimal Viewer
import { useState } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;
export default function SimplePdf({ fileUrl }: { fileUrl: string }) {
const [numPages, setNumPages] = useState<number>(0);
return (
<div className="w-full max-w-4xl mx-auto">
<Document file={fileUrl} onLoadSuccess={({ numPages }) => setNumPages(numPages)}>
{Array.from({ length: numPages }, (_, i) => (
<Page key={i} pageNumber={i + 1} width={900} />
))}
</Document>
</div>
);
}
Why choose it?
- Componentized API for React
- Built‑in text selection and (basic) form support
- Good community docs
Watch for
- Disable SSR for the viewer component (Next.js). Use
dynamic(() => import('./SimplePdf'), { ssr: false }). - Large files: render current + nearby pages only to avoid memory spikes.
Virtualized Example (pseudo‑code)
// Inside your viewer, only render pages in viewport range
const visiblePages = useVirtualPages(numPages, containerRef);
return visiblePages.map(n => <Page pageNumber={n} key={n} />);
3) Headless: PDF.js (pdfjs-dist) with Custom UI
Install
npm i pdfjs-dist
Skeleton
import { useEffect, useRef } from 'react';
import { GlobalWorkerOptions, getDocument, version } from 'pdfjs-dist';
import 'pdfjs-dist/web/pdf_viewer.css';
GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${version}/build/pdf.worker.min.js`;
export default function HeadlessPdf({ fileUrl }: { fileUrl: string }) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
let cancelled = false;
(async () => {
const pdf = await getDocument(fileUrl).promise;
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 1.5 });
const canvas = canvasRef.current!;
const ctx = canvas.getContext('2d')!;
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: ctx, viewport }).promise;
})();
return () => { cancelled = true; };
}, [fileUrl]);
return <canvas ref={canvasRef} className="w-full h-auto" />;
}
Why headless?
- Full control over layout, virtualization, page cache, search indexing, and UI.
- Tailor performance for massive docs.
Caveats
- You build the toolbar, thumbnails, text selection, and accessibility affordances.
4) Full‑Featured Commercial SDKs
When you need annotations, comments, collaboration, e‑signing, redaction, measurement, forms, or Office file viewing, consider:
- Apryse WebViewer (formerly PDFTron)
- PSPDFKit for Web
- Foxit Web SDK
Why: battle‑tested rendering, enterprise features, support SLAs, and consistent UX across browsers.
Cost trade‑off: higher licensing; heavier bundles. Many offer cloud/on‑prem options and per‑seat or usage licensing.
Next.js / Vite Integration Notes
- Disable SSR for viewer components. Example:
// app/pdf-viewer/page.tsx
import dynamic from 'next/dynamic';
const ClientPdf = dynamic(() => import('./ClientPdf'), { ssr: false });
export default function Page() { return <ClientPdf fileUrl="/files/sample.pdf" />; }
- Workers must be served correctly. If hosting yourself, copy
pdf.worker.min.jstopublic/and setworkerSrcaccordingly. - Range Requests: serve PDFs via a CDN/origin that supports
Accept-Ranges: bytes. - CORS: if loading cross‑origin, enable CORS and set
Content-Type: application/pdf. - Printing: PDF.js has its own print flow; test on Safari and iOS specifically.
Accessibility & Internationalization
- Provide keyboard navigation (page up/down, space, arrows).
- Ensure text layer is present for selection and screen readers (PDF.js text layer).
- Add aria labels for toolbar controls.
- Support RTL locales and mixed‑language content.
Performance Checklist
- Use virtualized pages (render only what’s visible).
- Cache rendered canvases; reuse when zooming.
- Preload next/prev page on idle.
- Keep scale < 2.0 on mobile to avoid massive canvases.
- Debounce zoom/scroll events.
- Offload heavy work to Web Workers.
Security Checklist
- Don’t
evalPDF contents; treat links as untrusted. - Sanitize
mailto:/javascript:links; open external links inrel="noopener noreferrer". - Respect
Content-Security-Policy. - Avoid loading untrusted PDFs from file:// origins in production.
- If embedding third‑party viewers, watch
X-Frame-Optionsandframe-ancestors.
Choosing: A Decision Tree
- Just preview a PDF? Try
<object>/<iframe>(fast) → If you need zoom/print consistency, goreact-pdf. - Custom UI & performance tuning? Build with
pdfjs-distdirectly; add virtualization. - Annotations/forms/signing/redaction? Choose a commercial SDK (Apryse/PSPDFKit/Foxit).
- Massive files / mobile heavy? Headless PDF.js + virtualization + worker + CDN with byte‑range.
Gotchas & Debugging
- Blank pages: worker not found → double‑check
workerSrcpath. - Slow first paint: no range requests → enable byte‑ranges, or load from same origin.
- CORS errors: add
Access-Control-Allow-Originand correctContent-Type. - SSR crash: dynamic import with
ssr:falseand guard allwindow/documentaccess. - Huge memory: render 1–3 pages around viewport; recycle canvases.
Quick Recommendations
- General product UI:
react-pdf+ light toolbar; add virtualization when PDFs exceed ~200 pages. - Heavily customized experience:
pdfjs-dist+ your own UI/virtualization. - Enterprise doc workflows: Apryse/PSPDFKit/Foxit—budget permitting.
Further Reading / Starters
- react-pdf docs; pdfjs-dist examples (mozilla)
- Apryse WebViewer, PSPDFKit for Web, Foxit Web SDK feature lists