Build a React PDF viewer with pdfjs-dist and Next.js: Step-by-step tutorial
Table of contents
Learn how to build a PDF viewer in React using pdfjs-dist (the official PDF.js npm package) and Next.js, and see how it compares to Nutrient’s commercial SDK.
- pdfjs-dist (open source) — Lightweight and flexible, but requires manual setup for things like annotations, search, navigation, or multipage viewing.
- Nutrient SDK — Provides 30+ built-in features, including annotation tools, PDF editing, form filling, and Office file support, all with minimal configuration.
This guide walks you through both setups with working code samples, performance tips, and a side-by-side comparison to help you choose the right tool for your use case.
In this tutorial, you’ll learn how to create a React PDF viewer using pdfjs-dist(opens in a new tab), the official npm distribution of PDF.js(opens in a new tab) — Mozilla’s open source JavaScript library for rendering PDF documents. You’ll use Next.js, a popular React framework, to demonstrate how to integrate pdfjs-dist into a React application.
You’ll first create a PDF viewer using pdfjs-dist and then compare it to a viewer built with Nutrient Web SDK. If you need to go beyond basic rendering, Nutrient lets you:
- Ship faster — Drop in a prebuilt, customizable user interface (UI) instead of building viewer chrome from scratch.
- Let users annotate — Give users 15+ annotation tools (highlight, comment, draw, stamp) without writing a custom toolbar.
- Handle more than PDFs — Open MS Office documents and images in the same viewer, so you don’t need a separate conversion pipeline.
- Add forms and signatures — Support PDF form filling and digital signatures out of the box.
- Collaborate in real time — Enable live multiuser editing without building your own sync layer.
Introduction to PDF.js and its capabilities
PDF.js(opens in a new tab) is an open source JavaScript library developed by Mozilla that renders PDF documents directly in web browsers. It’s distributed on npm as pdfjs-dist(opens in a new tab). PDF.js covers most common PDF viewing needs, including text selection, zooming, and page navigation.
Why choose PDF.js for React PDF viewing?
PDF.js works well in React applications. Installing pdfjs-dist via npm gives you the core rendering engine and web worker scripts. The web worker approach keeps rendering off the main thread. The project has an active contributor base and frequent releases.
Integrating a PDF.js viewer with React
PDF.js is organized into three main layers:
- Core — Handles the parsing of the binary PDF file. This layer isn’t usually relevant to users, as the library uses it internally.
- Display — Uses the core layer and exposes a more straightforward API to render PDFs and expose details.
- Viewer — Built on top of the display layer, this is the UI for the PDF viewer in Firefox and the other browser extensions within the project.
Prerequisites
- Node.js (v18 or later) and npm installed on your machine
- Basic familiarity with React and the command line
Setting up a React project with pdfjs-dist
Before rendering PDFs, set up your development environment. You’ll use Next.js, a popular React framework, to create the project structure.
Run the following command to scaffold a new Next.js project inside a folder named
pdfjs-demo:Terminal window npx create-next-app@latest pdfjs-demoWhen prompted, press Enter to accept the default options. This will set up the project with the recommended settings, allowing you to focus on integrating PDF.js with Next.js.
Next, navigate into your project directory and install the distribution build of PDF.js, available on npm as
pdfjs-dist(opens in a new tab):Terminal window cd pdfjs-demo && npm install pdfjs-distPDF.js uses web workers to offload the parsing and rendering of PDFs to a background thread, improving performance. To keep the setup simple, manually copy the worker script to the
publicdirectory, which is served statically by Next.js. Run the following command:Terminal window cp ./node_modules/pdfjs-dist/build/pdf.worker.min.mjs ./public/To automate this step, you can add a script to your
package.jsonfile. This script will copy the worker file whenever you run the dev or build commands:"scripts": {"copy-assets": "cp ./node_modules/pdfjs-dist/build/pdf.worker.min.mjs ./public/","dev": "next dev","build": "next build","dev": "npm run copy-assets && next dev","build": "npm run copy-assets && next build",}This copies the worker script to the public directory on every dev or build run.
Rendering a PDF in the application
With the setup complete, you can now implement the functionality to render a PDF. Start by creating a React component in the src/app/page.js file. This component will load a PDF file and render its first page on a canvas.
The following code defines a React component that uses the useEffect hook to load and render the first page of a PDF document on a canvas:
"use client";68 collapsed lines
import { useEffect, useRef } from "react";
export default function App() { const canvasRef = useRef(null); const renderTaskRef = useRef(null); // Ref to store the current render task.
useEffect(() => { let isCancelled = false;
(async function () { // Import pdfjs-dist dynamically for client-side rendering. const pdfJS = await import("pdfjs-dist");
// Set up the worker. pdfJS.GlobalWorkerOptions.workerSrc = window.location.origin + "/pdf.worker.min.mjs";
// Load the PDF document. const pdf = await pdfJS.getDocument("example.pdf").promise;
// Get the first page. const page = await pdf.getPage(1); const viewport = page.getViewport({ scale: 1.5 });
// Prepare the canvas. const canvas = canvasRef.current; const canvasContext = canvas.getContext("2d"); canvas.height = viewport.height; canvas.width = viewport.width;
// Ensure no other render tasks are running. if (renderTaskRef.current) { await renderTaskRef.current.promise; }
// Render the page into the canvas. const renderContext = { canvasContext, viewport }; const renderTask = page.render(renderContext);
// Store the render task. renderTaskRef.current = renderTask;
// Wait for rendering to finish. try { await renderTask.promise; } catch (error) { if (error.name === "RenderingCancelledException") { console.log("Rendering cancelled."); } else { console.error("Render error:", error); } }
if (!isCancelled) { console.log("Rendering completed"); } })();
// Cleanup function to cancel the render task if the component unmounts. return () => { isCancelled = true; if (renderTaskRef.current) { renderTaskRef.current.cancel(); } }; }, []);
return <canvas ref={canvasRef} style={{ height: "100vh" }} />;}The example.pdf file in the code above is present in the public directory. The useEffect hook with no dependencies gets executed once, just after the component is first rendered.
Note that the component tracks the current render task using a ref (renderTaskRef) and ensures that multiple render operations don’t overlap. If a new render task starts while the previous one is still running, the new task waits for the previous one to complete.
You can now run the application using npm run dev, and the application will open on the localhost:3000 port.
As you can see, you can only display the first page of your document by default. If you need any other functionality — like page navigation, search, or annotations — you’ll have to implement it yourself.
Handling common issues with PDF.js in React
When integrating PDF.js into a React application, developers may face several challenges. Here are common issues and how to fix them.
Rendering issues
Problem
PDF.js relies on a <canvas> element to render PDF pages. Issues often arise with canvas size, scaling, and responsiveness within React components.
Solutions
Proper sizing — Ensure the canvas element is correctly sized to fit its container. Use CSS or inline styles to set the dimensions, and consider using React’s
useEffectto adjust the size based on the window or container size.Scaling — PDF.js provides scaling options to render PDFs at different zoom levels. Configure the scale factor properly to ensure the content is rendered clearly and fits within the viewable area.
Viewport management — Use PDF.js’s viewport settings to manage how pages are displayed. Adjust the viewport based on the container size or user interactions to maintain a responsive design.
Example code:
useEffect(() => { const renderPage = (pageNumber) => { pdf.getPage(pageNumber).then((page) => { const viewport = page.getViewport({ scale: 1.5 }); const canvas = canvasRef.current; const context = canvas.getContext("2d"); canvas.height = viewport.height; canvas.width = viewport.width;
const renderContext = { canvasContext: context, viewport: viewport, }; page.render(renderContext); }); };
renderPage(1);}, []);Worker setup
Problem
PDF.js uses web workers to handle PDF rendering tasks off the main thread. Incorrect setup or configuration of these workers can lead to performance issues or failures in rendering.
Solutions
- Worker path — Ensure the path to the PDF.js worker file is correctly specified. The worker script must be accessible from the client-side.
- Web worker configuration — Configure PDF.js to use web workers by setting the
workerSrcproperty correctly. This can be done in your React component or globally in your application setup.
Example code:
import * as pdfjsLib from "pdfjs-dist";pdfjsLib.GlobalWorkerOptions.workerSrc = "/path-to/pdf.worker.min.mjs";Cross-browser compatibility
Problem
Different browsers may have varying levels of support for HTML5 features used by PDF.js, leading to inconsistencies in rendering or functionality.
Solutions
- Testing — Regularly test your application on multiple browsers (Chrome, Firefox, Safari, Edge) to ensure consistent behavior and appearance.
- Polyfills — Use polyfills for missing or inconsistent features in certain browsers. This can help ensure compatibility across different environments.
- Feature detection — Implement feature detection to provide fallback solutions or graceful degradation if certain features aren’t supported.
Example code:
if (!("HTMLCanvasElement" in window)) { console.error("Canvas not supported"); // Provide fallback or notification.}Troubleshooting pdfjs-dist in Next.js
If you run into issues integrating pdfjs-dist with Next.js, here are the most common problems and how to fix them.
Worker file not found (404)
Problem — The browser console shows a 404 error for pdf.worker.min.mjs.
Fix — Make sure you’ve copied the worker file to your public/ directory. The path in workerSrc must match the actual file location:
// Correct — file must exist at public/pdf.worker.min.mjs.pdfJS.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.mjs";If you upgraded pdfjs-dist, the worker file name may have changed. Re-run the copy command:
cp ./node_modules/pdfjs-dist/build/pdf.worker.min.mjs ./public/Missing “use client” directive
Problem — Next.js throws a server-side rendering error because pdfjs-dist accesses browser APIs (window, document, canvas) that don’t exist on the server.
Fix — Add "use client" at the top of any component that imports or uses pdfjs-dist:
"use client";import { useEffect, useRef } from "react";// Now safe to use pdfjs-dist here.Version mismatch between pdfjs-dist and its worker
Problem — The viewer loads but pages render as blank, or the console shows Setting up fake worker warnings.
Fix — The pdfjs-dist package version and the worker script version must match exactly. After upgrading, always recopy the worker file and confirm the versions align:
npm ls pdfjs-distcp ./node_modules/pdfjs-dist/build/pdf.worker.min.mjs ./public/Canvas hydration mismatch
Problem — Next.js warns about a hydration mismatch because the server-rendered HTML doesn’t include canvas content.
Fix — Wrap your PDF rendering in a dynamic import with SSR disabled, or use conditional rendering:
import dynamic from "next/dynamic";
const PDFViewer = dynamic(() => import("./PDFViewer"), { ssr: false,});These issues are specific to using raw pdfjs-dist. With Nutrient Web SDK, worker configuration, SSR handling, and asset management are handled automatically — so you can focus on building your application instead of debugging integration issues.
Setting up Nutrient Web SDK with Next.js
If the pdfjs-dist setup above feels like a lot of work for just displaying a single page, Nutrient’s React PDF viewer gives you a production-ready viewer — with annotations, form filling, signatures, Office file support, and real-time collaboration — without building any of it yourself.
Follow the steps below to set it up in the same amount of time.
Start a new project using
create-next-app:Terminal window npx create-next-app@latest nutrient-democd nutrient-demoIn
app/layout.js, load Nutrient Web SDK from the CDN using Next.js’s<Script>component. No extra dependencies or webpack configuration are needed:import Script from "next/script";export default function RootLayout({ children }) {return (<html lang="en"><body><Scriptsrc="https://cdn.cloud.pspdfkit.com/pspdfkit-web/latest/nutrient-viewer.js"strategy="beforeInteractive"/>{children}</body></html>);}Render a PDF in
app/page.js:"use client";import { useEffect, useRef } from "react";export default function Home() {const containerRef = useRef(null);useEffect(() => {const container = containerRef.current;if (container && window.NutrientViewer) {window.NutrientViewer.load({container,document: "example.pdf", // Place this file in the public/ directory.});}return () => {if (window.NutrientViewer && container) {window.NutrientViewer.unload(container);}};}, []);return (<div ref={containerRef} style={{ height: "100vh", width: "100%" }} />);}Start the development server:
Terminal window npm run dev
Visit http://localhost:3000, where you’ll see the PDF rendered using Nutrient Web SDK.
That’s it — no webpack plugins, no asset copying, no extra build scripts. The CDN handles loading the SDK and its assets automatically.
Performance and security best practices for React PDF viewers
Follow these practices when building a React PDF viewer:
- Use a custom PDF worker URL — Offload heavy parsing and rendering tasks to a separate thread to keep the UI responsive.
- Serve over HTTPS — Always deliver your app and PDF files over HTTPS to protect against tampering and interception.
- Apply a Content Security Policy (CSP) — Limit allowed sources for scripts, workers, and fonts to mitigate XSS attacks.
- Enable a Web Application Firewall (WAF) — Add an extra security layer to detect and block common exploits at the network level.
Nutrient adds encryption, CSP support, and secure deployment guidance for production use.
pdfjs-dist vs. Nutrient: Side-by-side comparison
The table below compares pdfjs-dist and Nutrient by what you can deliver to your users, not just what each library includes.
| What you need to deliver | pdfjs-dist | Nutrient SDK |
|---|---|---|
| Display a PDF in the browser | Manual canvas setup, worker configuration, and render-task management | One-line NutrientViewer.load() call with built-in UI |
| Let users annotate documents | Build a custom toolbar and annotation engine from scratch | 15+ annotation tools ready to use |
| Fill and submit PDF forms | Not supported — you’d need a separate library | Built-in form filler with validation |
| Collect signatures | Not supported | Digital and electronic signatures |
| Open Office files (DOCX, XLSX, PPTX) | Not supported — requires a server-side conversion step | Client-side Office viewing in the same viewer |
| Enable real-time collaboration | Build your own sync layer with WebSockets or CRDT | Built-in real-time sync |
| Search text inside a PDF | Implement text layer extraction and search UI manually | Built-in search with highlighting |
| Go to production with confidence | You maintain rendering, workers, and cross-browser fixes | Dedicated support team and enterprise-grade security |
When to use pdfjs-dist vs. Nutrient
Choose pdfjs-dist if:
- You only need to display PDFs — no annotations, forms, or signatures.
- You want full control over the rendering pipeline and UI.
- Your team has the bandwidth to build and maintain custom viewer features.
- You’re building a lightweight prototype or internal tool.
Choose Nutrient if:
- You need annotations, form filling, or digital signatures and don’t want to build them yourself.
- You need to open Office files or images in the same viewer as PDFs.
- You’re on a deadline and want a production-ready viewer out of the box.
- You need enterprise support, SLAs, or compliance guarantees.
- You want real-time collaboration without building a sync layer.
Conclusion
In this tutorial, you built a React PDF viewer two ways: first with pdfjs-dist, where you configured workers, managed canvas rendering, and handled cleanup manually; then with Nutrient Web SDK, where a single load() call gave you a complete viewer with annotations, forms, and signatures included.
pdfjs-dist is a solid choice when you only need to display PDFs and want full control over every detail. But if your users need to annotate, fill forms, sign documents, or collaborate — and you’d rather ship those features this week instead of building them over the next quarter — Nutrient’s PDF SDK handles all of that out of the box.
Try it for free, or see the live demo to explore what’s included.
We created similar how-to blog posts using different web frameworks and libraries:
- How to build an Angular PDF viewer with PDF.js
- How to build a Vue.js PDF viewer with PDF.js
- How to build a jQuery PDF viewer with PDF.js
- How to build a Bootstrap 5 PDF viewer with PDF.js
- How to build an Electron PDF viewer with PDF.js
- How to build a TypeScript PDF viewer with PDF.js
- How to build a JavaScript PDF viewer with PDF.js
FAQ
pdfjs-dist is the official npm package for PDF.js, Mozilla’s open source PDF rendering library. It includes the prebuilt display and worker layers, so you can install it with npm install pdfjs-dist and start rendering PDFs in your React, Next.js, or vanilla JavaScript project without building PDF.js from source.
PDF.js uses web workers to handle the heavy lifting of PDF parsing and rendering in a separate thread. This prevents the main thread from being blocked, keeping the UI responsive during rendering.
Yes. You can customize the PDF viewer by styling the components with CSS and adding features like text search, annotations, and custom navigation controls using the PDF.js API.
Common challenges include managing state and performance, handling large or complex PDF documents, ensuring cross-browser compatibility, and implementing advanced features like text extraction and annotations.
The Nutrient SDK is a commercial PDF library designed for developers who need advanced features like annotations, form filling, digital signatures, collaboration, and support for Office and image files. Unlike PDF.js, which requires manual implementation for most features, Nutrient offers more than 30 out-of-the-box tools with minimal configuration.
You can integrate the Nutrient SDK by installing the @nutrient-sdk/viewer package and including its script in your layout using Next.js’s <Script> component. Then, use the NutrientViewer.load() method to render a PDF or DOCX inside a React component. We provide full instructions and copy-ready code samples in the tutorial.
The Nutrient SDK includes built-in support for annotations, PDF editing, form creation and filling, electronic and digital signatures, PDF/A generation, document redaction, real-time collaboration, and Office document viewing — features that require significant custom development in PDF.js.
Yes. Nutrient supports encryption, CSP headers, secure deployment practices, and common compliance standards. Detailed guidance is available in our security documentation(opens in a new tab).
The most common cause is a missing or mismatched worker file. Make sure you copy the worker script from node_modules/pdfjs-dist/build/pdf.worker.min.mjs to your public/ directory, and that the version matches your installed pdfjs-dist package. Also ensure your component includes "use client" at the top, since pdfjs-dist requires browser APIs that aren’t available during server-side rendering.
pdfjs-dist is the raw PDF.js library from Mozilla — it gives you low-level control over PDF rendering via canvas but requires you to build the UI yourself. react-pdf (by wojtekmaj) is a React wrapper around pdfjs-dist that provides <Document> and <Page> components for easier integration. Both are open source and limited to rendering; neither include annotations, form filling, or signatures.
Yes. Nutrient offers a free trial when you sign up. You can evaluate all major features — including viewing, annotation, form filling, and PDF export — directly in your local or production environment.