Sunday, October 19, 2025

React PDF Viewers: The 2025 Guide

Akash Milton
react pdf viewer 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-dist directly 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.js to public/ and set workerSrc accordingly.
  • 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 eval PDF contents; treat links as untrusted.
  • Sanitize mailto:/javascript: links; open external links in rel="noopener noreferrer".
  • Respect Content-Security-Policy.
  • Avoid loading untrusted PDFs from file:// origins in production.
  • If embedding third‑party viewers, watch X-Frame-Options and frame-ancestors.

Choosing: A Decision Tree

  1. Just preview a PDF? Try <object>/<iframe> (fast) → If you need zoom/print consistency, go react-pdf.
  2. Custom UI & performance tuning? Build with pdfjs-dist directly; add virtualization.
  3. Annotations/forms/signing/redaction? Choose a commercial SDK (Apryse/PSPDFKit/Foxit).
  4. Massive files / mobile heavy? Headless PDF.js + virtualization + worker + CDN with byte‑range.

Gotchas & Debugging

  • Blank pages: worker not found → double‑check workerSrc path.
  • Slow first paint: no range requests → enable byte‑ranges, or load from same origin.
  • CORS errors: add Access-Control-Allow-Origin and correct Content-Type.
  • SSR crash: dynamic import with ssr:false and guard all window/document access.
  • 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