React PDF viewer with pdfjs-dist and Next.js (2026): Step-by-step tutorial
Table of contents
Nutrient Web SDK renders, annotates, and fills forms client side. Try the live demo - no signup required.
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) — This tutorial uses its display API for first-page canvas rendering, with worker setup and effect cleanup.- Nutrient SDK — Provides a viewer UI with annotation, editing, and form tools; licensed components and deployment requirements vary.
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.
This example uses the PDF.js display API to draw one page on a canvas. It doesn’t implement text selection, search, or a toolbar. For React components with navigation and thumbnails, use the react-pdf tutorial.
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 17+ 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 — Integrate PDF form filling and digital signatures using the SDK’s APIs and viewer UI.
- 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 worker handles PDF parsing and related processing; drawing to the canvas still involves 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 provides 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 (22.13 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@16.3.5 pdfjs-demo --js --app --src-dir --use-npm --no-tailwind --yesThese options select JavaScript, the App Router, and a
srcdirectory, matching the file paths below.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-demonpm install pdfjs-dist@6.3.289PDF.js uses web workers to move PDF parsing and related processing 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
publicdirectory 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";
import { useEffect, useRef, useState } from "react";
export default function App() { const hostRef = useRef(null); const [error, setError] = useState("");
useEffect(() => { const host = hostRef.current; if (!host) return; const canvas = document.createElement("canvas"); canvas.style.maxWidth = "100%"; canvas.style.height = "auto"; canvas.setAttribute("aria-label", "First page of the PDF"); host.appendChild(canvas); let cancelled = false; let loadingTask; let renderTask;
async function renderPDF() { try { const pdfJS = await import("pdfjs-dist"); if (cancelled) return; pdfJS.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.mjs"; loadingTask = pdfJS.getDocument({ url: "/example.pdf" }); const pdf = await loadingTask.promise; if (cancelled) return; const page = await pdf.getPage(1); if (cancelled) return; const viewport = page.getViewport({ scale: 1.5 }); canvas.width = Math.ceil(viewport.width); canvas.height = Math.ceil(viewport.height); renderTask = page.render({ canvas, viewport }); await renderTask.promise; } catch (cause) { if (!cancelled) setError(String(cause)); } } void renderPDF();
return () => { cancelled = true; renderTask?.cancel(); void loadingTask?.destroy().catch(console.error); canvas.remove(); }; }, []);
return ( <main> {error && <p role="alert">Failed to load PDF: {error}</p>} <div ref={hostRef} /> <a href="/example.pdf">Open the original PDF</a> </main> );}Place a PDF at public/example.pdf. Then run npm run dev and open http://localhost:3000. The example displays the first page. Each effect owns a canvas and cancels its render task and loading task during cleanup, including React Strict Mode’s development setup/cleanup cycle.
The canvas doesn’t provide selectable or accessible document text. A production viewer needs a text layer and suitable navigation, or you can integrate the complete PDF.js viewer. The link in the example provides access to the original document.
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.
The main example sets the canvas dimensions from the PDF viewport and preserves the aspect ratio with CSS. If you add zoom or page changes, cancel the old render before drawing the next page.
Worker setup
Problem
PDF.js uses web workers for parsing and related processing. 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:
// Run inside the client-side effect.const pdfjsLib = await import("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, and 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. Rerun 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" for React hooks, and keep the PDF.js import inside useEffect. Client components can still be prerendered on the server:
"use client";import { useEffect, useRef } from "react";// Import `pdfjs-dist` dynamically inside `useEffect`, as shown above.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/Hydration warnings
Drawing into an initially empty canvas after mounting doesn’t itself cause a hydration mismatch. Check whether the server and initial client render produce different HTML. Keep browser API access inside effects. If using next/dynamic with ssr: false, declare that dynamic component inside a client component.
Both PDF.js and Nutrient need browser-only initialization in Next.js. Their worker and asset setup differs.
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 — reducing the viewer UI your team needs to build and maintain. Available features depend on licensing and deployment configuration.
Create a separate JavaScript App Router project and install the SDK:
npx create-next-app@16.3.5 nutrient-demo --js --app --src-dir --use-npm --no-tailwind --yescd nutrient-demonpm install @nutrient-sdk/viewer@1.22.0Download the matching runtime assets archive(opens in a new tab) and extract its contents into public, preserving the nutrient-viewer-lib directory. On macOS or Linux, run these commands from the project root:
curl --fail --location https://cdn.cloud.nutrient.io/assets/pspdfkit-web-assets@1.22.0.zip -o nutrient-assets.zipunzip nutrient-assets.zip -d publicOn Windows, download the same ZIP and extract it into public using your archive tool. This example serves runtime assets from the app’s origin. See the asset hosting guide for deployment options. The paths below assume the app is hosted at the domain root.
Place a PDF at public/example.pdf. Replace src/app/page.js with the following. The browser-only import runs inside the effect; cleanup unloads the container owned by that effect:
"use client";
import { useEffect, useRef, useState } from "react";
export default function App() { const hostRef = useRef(null); const [error, setError] = useState("");
useEffect(() => { const host = hostRef.current; if (!host) return;
// Each effect owns a container, including during Strict Mode cleanup. const container = document.createElement("div"); container.style.height = "100%"; host.appendChild(container); let cancelled = false; let NutrientViewer;
async function loadViewer() { try { NutrientViewer = (await import("@nutrient-sdk/viewer")).default; if (cancelled) return; await NutrientViewer.load({ container, document: "/example.pdf", baseUrl: `${window.location.origin}/`, }); if (cancelled) NutrientViewer.unload(container); } catch (cause) { if (!cancelled) setError(String(cause)); } } void loadViewer();
return () => { cancelled = true; NutrientViewer?.unload(container); container.remove(); }; }, []);
return ( <> {error && <p role="alert">Failed to load PDF: {error}</p>} <div ref={hostRef} style={{ height: "100vh", width: "100%" }} /> </> );}Run npm run dev and open http://localhost:3000. Licensed features and collaboration setup vary by deployment.
Performance and security best practices for React PDF viewers
Follow these practices when building a React PDF viewer:
- Use a custom PDF worker URL — Move PDF parsing and related processing 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 cross-site scripting (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 | This canvas example has no annotation UI; the complete PDF.js viewer includes annotation tools | 17+ annotation tools ready to use |
| Fill and submit PDF forms | Not implemented here; the complete PDF.js viewer supports interactive forms | Built-in form filler with validation |
| Collect signatures | Not implemented in this canvas example | Digital and electronic signatures |
| Open Office files (DOCX, XLSX, PPTX) | Not supported — requires a separate conversion step | Client-side Office viewing in the same viewer |
| Enable real-time collaboration | Build your own sync layer with WebSockets or a conflict-free replicated data type (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:
- The canvas example meets your display requirements, or you plan to integrate the full PDF.js viewer for its form and annotation tools.
- You want full control over the rendering pipeline and UI.
- Your team has the bandwidth to build and maintain custom viewer features.
- You can maintain and test the viewer integration for your application’s requirements.
Choose Nutrient if:
- You need APIs and UI components to capture, persist, and integrate annotations, form data, or signature workflows.
- You need to open Office files or images in the same viewer as PDFs.
- You want a prebuilt viewer UI that reduces custom implementation work.
- You need commercial support and documented deployment options.
- You want real-time collaboration without building a sync layer.
Still deciding? These five signs tell you when you’ve outgrown PDF.js and should move to a commercial SDK.
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 load() call initialized a viewer UI. Additional features depend on licensing and configuration.
Use the display API when you want direct control over rendering and lifecycle management. Consider a complete viewer when you need a toolbar and document workflows. Compare supported features using your own documents and licensing requirements.
Try it for free, or see the live demo to explore what’s included.
For other ways to view PDFs in React, see how to build a React PDF viewer with Nutrient, the react-pdf library tutorial, or how to build a React file viewer.
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
- How to build a Next.js PDF viewer
FAQ
pdfjs-dist, and how does it relate to PDF.js?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 a worker for PDF parsing and related processing. Canvas drawing still involves the main thread, so large pages can affect responsiveness.
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.
See our step-by-step migration guide for side-by-side code comparisons covering viewer setup, search, annotations, forms, and more.
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. Compare the complete PDF.js viewer with Nutrient’s licensed capabilities when choosing a production integration.
You can integrate Nutrient SDK by installing the @nutrient-sdk/viewer package and importing it dynamically inside a client component’s useEffect. 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.
Nutrient offers document editing, redaction, digital signature workflows, collaboration, and Office viewing through its licensed components and deployment options. PDF.js also has form and annotation capabilities, so compare specific workflows rather than treating it as a rendering-only library.
Review Nutrient’s security documentation alongside your application’s requirements. Compliance depends on the complete deployment, data handling, and operational controls.
pdfjs-dist worker errors in Next.js?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. Include "use client" and import PDF.js inside useEffect so browser APIs aren’t accessed during server prerendering.
pdfjs-dist and react-pdf?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. The canvas example here omits higher-level features. The complete PDF.js viewer supports forms and annotation tools, while react-pdf exposes an annotation layer and optional interactive forms.
Start with the Nutrient Web SDK evaluation guide. Confirm which components your trial includes and test them with your documents. Review the licensing requirements before deploying to production.