This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /blog/how-to-build-a-reactjs-viewer-with-pdfjs.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. React PDF viewer with pdfjs-dist and Next.js (2026): Step-by-step tutorial

Table of contents

    Need to add PDF viewing to your React or Next.js app? This tutorial walks you through creating a custom PDF viewer using pdfjs-dist, the official npm package for Mozilla’s open source PDF.js library, and compares it to Nutrient’s commercial SDK — perfect if you need annotations, editing, or real-time collaboration.
    React PDF viewer with pdfjs-dist and Next.js (2026): Step-by-step tutorial
    See a full-featured PDF viewer in React

    Nutrient Web SDK renders, annotates, and fills forms client side. Try the live demo - no signup required.

    Summary

    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:

    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.

    1. 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 --yes

      These options select JavaScript, the App Router, and a src directory, matching the file paths below.

    2. 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-dist@6.3.289
    3. PDF.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 public directory, 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.json file. 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";
    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 useEffect to 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 workerSrc property 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:

    Terminal window
    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:

    Terminal window
    npm ls pdfjs-dist
    cp ./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:

    Terminal window
    npx create-next-app@16.3.5 nutrient-demo --js --app --src-dir --use-npm --no-tailwind --yes
    cd nutrient-demo
    npm install @nutrient-sdk/viewer@1.22.0

    Download 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:

    Terminal window
    curl --fail --location https://cdn.cloud.nutrient.io/assets/pspdfkit-web-assets@1.22.0.zip -o nutrient-assets.zip
    unzip nutrient-assets.zip -d public

    On 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 deliverpdfjs-distNutrient SDK
    Display a PDF in the browserManual canvas setup, worker configuration, and render-task managementOne-line NutrientViewer.load() call with built-in UI
    Let users annotate documentsThis canvas example has no annotation UI; the complete PDF.js viewer includes annotation tools17+ annotation tools ready to use
    Fill and submit PDF formsNot implemented here; the complete PDF.js viewer supports interactive formsBuilt-in form filler with validation
    Collect signaturesNot implemented in this canvas exampleDigital and electronic signatures
    Open Office files (DOCX, XLSX, PPTX)Not supported — requires a separate conversion stepClient-side Office viewing in the same viewer
    Enable real-time collaborationBuild your own sync layer with WebSockets or a conflict-free replicated data type (CRDT)Built-in real-time sync
    Search text inside a PDFImplement text layer extraction and search UI manuallyBuilt-in search with highlighting
    Go to production with confidenceYou maintain rendering, workers, and cross-browser fixesDedicated 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:

    FAQ

    What’s 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.

    Why do I need to use a worker script with PDF.js?

    PDF.js uses a worker for PDF parsing and related processing. Canvas drawing still involves the main thread, so large pages can affect responsiveness.

    Can I customize the appearance and functionality of the PDF viewer?

    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.

    What are some common challenges when building a PDF viewer with React.js and PDF.js?

    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.

    How do I migrate from PDF.js to Nutrient?

    See our step-by-step migration guide for side-by-side code comparisons covering viewer setup, search, annotations, forms, and more.

    What is the Nutrient SDK, and how is it different from PDF.js?

    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.

    How do I use Nutrient SDK in a React or Next.js app?

    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.

    What features does Nutrient SDK support that PDF.js doesn’t?

    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.

    Is Nutrient secure and compliant for enterprise use?

    Review Nutrient’s security documentation alongside your application’s requirements. Compliance depends on the complete deployment, data handling, and operational controls.

    How do I fix 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.

    What’s the difference between 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.

    Can I try the Nutrient SDK for free?

    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.

    Ritesh Kumar

    Ritesh Kumar

    Web Senior Staff Software Engineer

    Ritesh loves to write code, play keyboard, and paint. He likes working on projects that involve developer tooling, design systems, and music. He wants to make art a part of everyone’s life by using technology.

    Hulya Masharipov

    Hulya Masharipov

    Technical Writer

    Hulya is a frontend web developer and technical writer who enjoys creating responsive, scalable, and maintainable web experiences. She’s passionate about open source, web accessibility, cybersecurity privacy, and blockchain.

    Explore related topics

    Try for free Add a complete PDF viewer to your app