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

Table of contents

    Build a full-featured React PDF viewer from scratch with react-pdf. This tutorial walks through page navigation, zoom, thumbnails, outline navigation, error handling, and a TypeScript implementation. You’ll also see how Nutrient’s React PDF library compares when you need annotations, forms, or signatures.
    react-pdf tutorial: Build a PDF viewer in React (2026)
    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.

    TL;DR

    This tutorial builds a React PDF viewer with react-pdf step by step, including page navigation, zoom, thumbnails, outline, error handling, and TypeScript types. It then shows how Nutrient’s React PDF library handles the same task when you need annotations, forms, or signatures built in.

    What is react-pdf?

    react-pdf(opens in a new tab) is an open source React component library built on top of Mozilla’s PDF.js(opens in a new tab). It provides Document and Page components that handle PDF rendering, along with Outline and Thumbnail components for navigation. It’s maintained by Wojciech Maj(opens in a new tab).

    This tutorial uses react-pdf 10.5.0 and its versioned documentation(opens in a new tab). It displays existing PDFs; it’s a different package from @react-pdf/renderer, which creates PDFs. For direct worker and canvas control in Next.js, follow the PDF.js React tutorial.

    react-pdf provides rendering components rather than a complete viewer toolbar. Its annotation layer displays supported existing annotations, and <Page renderForms /> enables interactive forms. Annotation authoring, document editing, and signing need additional tooling.

    Prerequisites

    Before starting, make sure you have:

    Building a PDF viewer with react-pdf

    This section covers project setup, PDF rendering, and common viewer features. The complete App examples are standalone replacements for src/App.jsx, each demonstrating one feature. Replacing the file with the next example won’t retain earlier controls. For navigation, zoom, thumbnails, and an outline in one component, use the combined TypeScript example.

    Create a React project

    Scaffold a new React project with Vite:

    Terminal window
    npm create vite@latest react-pdf-demo -- --template react

    Change into the project directory and install dependencies:

    Terminal window
    cd react-pdf-demo
    npm install

    Install react-pdf and configure the worker

    Install the react-pdf package:

    Terminal window
    npm install react-pdf@10.5.0

    Place a PDF file in the public directory. You can use our demo document — rename it to document.pdf.

    react-pdf depends on a PDF.js web worker for rendering. Configure the worker in the same file where you use the Document component. You’ll set this up in the next step.

    Render a PDF document

    Open src/App.jsx and replace its contents with the following:

    import { useState } from "react";
    import { Document, Page, pdfjs } from "react-pdf";
    import "react-pdf/dist/Page/TextLayer.css";
    import "react-pdf/dist/Page/AnnotationLayer.css";
    pdfjs.GlobalWorkerOptions.workerSrc = new URL(
    "pdfjs-dist/build/pdf.worker.min.mjs",
    import.meta.url,
    ).toString();
    const App = () => {
    const [numPages, setNumPages] = useState(null);
    const [pageNumber, setPageNumber] = useState(1);
    const onDocumentLoadSuccess = ({ numPages }) => {
    setNumPages(numPages);
    };
    const goToPrevPage = () => setPageNumber((prev) => Math.max(prev - 1, 1));
    const goToNextPage = () =>
    setPageNumber((prev) => (numPages ? Math.min(prev + 1, numPages) : prev));
    return (
    <div>
    <nav>
    <button onClick={goToPrevPage}>Prev</button>
    <button onClick={goToNextPage}>Next</button>
    <p>
    Page {pageNumber} of {numPages}
    </p>
    </nav>
    <Document file="/document.pdf" onLoadSuccess={onDocumentLoadSuccess}>
    <Page pageNumber={pageNumber} />
    </Document>
    </div>
    );
    };
    export default App;

    This single file includes the worker configuration, text layer CSS, and annotation layer CSS. Document loads the PDF and exposes numPages through onLoadSuccess. Page renders one page at a time based on pageNumber.

    Start the development server:

    Terminal window
    npm run dev

    For another implementation, see GitHub(opens in a new tab).

    Add page navigation

    The code above already includes basic previous/next buttons. Here’s a more complete navigation bar with a page input field:

    <nav style={{ display: "flex", alignItems: "center", gap: "8px" }}>
    <button onClick={goToPrevPage} disabled={pageNumber <= 1}>
    Prev
    </button>
    <span>
    Page{" "}
    <input
    aria-label="Page number"
    type="number"
    min={1}
    max={numPages || 1}
    value={pageNumber}
    onChange={(e) => {
    const page = Number(e.target.value);
    if (Number.isInteger(page) && page >= 1 && page <= numPages) {
    setPageNumber(page);
    }
    }}
    style={{ width: "50px", textAlign: "center" }}
    />{" "}
    of {numPages}
    </span>
    <button onClick={goToNextPage} disabled={!numPages || pageNumber >= numPages}>
    Next
    </button>
    </nav>

    This adds an input field so users can jump directly to a page, and it disables the buttons at the document boundaries.

    Add zoom controls

    The Page component accepts a scale prop that controls the zoom level. Replace src/App.jsx with this standalone example to try zoom buttons:

    import { useState } from "react";
    import { Document, Page, pdfjs } from "react-pdf";
    import "react-pdf/dist/Page/TextLayer.css";
    import "react-pdf/dist/Page/AnnotationLayer.css";
    pdfjs.GlobalWorkerOptions.workerSrc = new URL(
    "pdfjs-dist/build/pdf.worker.min.mjs",
    import.meta.url,
    ).toString();
    const App = () => {
    const [numPages, setNumPages] = useState(null);
    const [pageNumber, setPageNumber] = useState(1);
    const [scale, setScale] = useState(1.0);
    const onDocumentLoadSuccess = ({ numPages }) => {
    setNumPages(numPages);
    };
    const goToPrevPage = () => setPageNumber((prev) => Math.max(prev - 1, 1));
    const goToNextPage = () =>
    setPageNumber((prev) => (numPages ? Math.min(prev + 1, numPages) : prev));
    const zoomIn = () => setScale((prev) => Math.min(prev + 0.25, 3));
    const zoomOut = () => setScale((prev) => Math.max(prev - 0.25, 0.5));
    const resetZoom = () => setScale(1.0);
    return (
    <div>
    <nav style={{ display: "flex", gap: "8px", alignItems: "center" }}>
    <button onClick={goToPrevPage} disabled={pageNumber <= 1}>
    Prev
    </button>
    <span>
    Page {pageNumber} of {numPages}
    </span>
    <button onClick={goToNextPage} disabled={!numPages || pageNumber >= numPages}>
    Next
    </button>
    <span>|</span>
    <button onClick={zoomOut} disabled={scale <= 0.5}>
    −
    </button>
    <span>{Math.round(scale * 100)}%</span>
    <button onClick={zoomIn} disabled={scale >= 3}>
    +
    </button>
    <button onClick={resetZoom}>Reset</button>
    </nav>
    <Document file="/document.pdf" onLoadSuccess={onDocumentLoadSuccess}>
    <Page pageNumber={pageNumber} scale={scale} />
    </Document>
    </div>
    );
    };
    export default App;

    The scale prop maps directly to the PDF.js render scale. A value of 1 is 100 percent, 1.5 is 150 percent, and so on. The buttons clamp the range between 50 percent and 300 percent.

    Add a thumbnail sidebar

    react-pdf exports a Thumbnail component that renders a small preview of a page. Replace src/App.jsx with this standalone sidebar example; it doesn’t include the previous zoom controls:

    import { useState } from "react";
    import { Document, Page, Thumbnail, pdfjs } from "react-pdf";
    import "react-pdf/dist/Page/TextLayer.css";
    import "react-pdf/dist/Page/AnnotationLayer.css";
    pdfjs.GlobalWorkerOptions.workerSrc = new URL(
    "pdfjs-dist/build/pdf.worker.min.mjs",
    import.meta.url,
    ).toString();
    const App = () => {
    const [numPages, setNumPages] = useState(null);
    const [pageNumber, setPageNumber] = useState(1);
    const onDocumentLoadSuccess = ({ numPages }) => {
    setNumPages(numPages);
    };
    return (
    <Document file="/document.pdf" onLoadSuccess={onDocumentLoadSuccess}>
    <div style={{ display: "flex", gap: "16px" }}>
    {/* Thumbnail sidebar. */}
    <div
    style={{
    width: "150px",
    overflowY: "auto",
    maxHeight: "80vh",
    borderRight: "1px solid #ccc",
    padding: "8px",
    }}
    >
    {numPages &&
    Array.from({ length: numPages }, (_, index) => (
    <div
    key={index}
    style={{
    cursor: "pointer",
    border:
    pageNumber === index + 1
    ? "2px solid #0078d4"
    : "2px solid transparent",
    marginBottom: "8px",
    }}
    >
    <Thumbnail pageNumber={index + 1} width={130} onItemClick={({ pageNumber }) => setPageNumber(pageNumber)} />
    </div>
    ))}
    </div>
    {/* Main page view. */}
    <div>
    <Page pageNumber={pageNumber} />
    </div>
    </div>
    </Document>
    );
    };
    export default App;

    The Thumbnail component accepts most of the same props as Page (including width for sizing). Clicking a thumbnail updates the pageNumber state to navigate to that page. The active thumbnail gets a blue border.

    Display the document outline

    If the PDF has bookmarks, the Outline component renders them as a clickable list. Replace src/App.jsx with this standalone outline example. Its onItemClick callback receives { pageNumber } for navigation; thumbnails and zoom controls aren’t included:

    import { useState } from "react";
    import { Document, Page, Outline, pdfjs } from "react-pdf";
    import "react-pdf/dist/Page/TextLayer.css";
    import "react-pdf/dist/Page/AnnotationLayer.css";
    pdfjs.GlobalWorkerOptions.workerSrc = new URL(
    "pdfjs-dist/build/pdf.worker.min.mjs",
    import.meta.url,
    ).toString();
    const App = () => {
    const [numPages, setNumPages] = useState(null);
    const [pageNumber, setPageNumber] = useState(1);
    const onDocumentLoadSuccess = ({ numPages }) => {
    setNumPages(numPages);
    };
    return (
    <Document file="/document.pdf" onLoadSuccess={onDocumentLoadSuccess}>
    <div style={{ display: "flex", gap: "16px" }}>
    {/* Outline sidebar. */}
    <div
    style={{
    width: "200px",
    overflowY: "auto",
    maxHeight: "80vh",
    borderRight: "1px solid #ccc",
    padding: "8px",
    }}
    >
    <h3>Table of Contents</h3>
    <Outline onItemClick={({ pageNumber: pg }) => setPageNumber(pg)} />
    </div>
    {/* Main page view. */}
    <div>
    <nav>
    <button
    onClick={() => setPageNumber((p) => Math.max(p - 1, 1))}
    disabled={pageNumber <= 1}
    >
    Prev
    </button>
    <span>
    Page {pageNumber} of {numPages}
    </span>
    <button
    onClick={() =>
    setPageNumber((p) => (numPages ? Math.min(p + 1, numPages) : p))
    }
    disabled={!numPages || pageNumber >= numPages}
    >
    Next
    </button>
    </nav>
    <Page pageNumber={pageNumber} />
    </div>
    </div>
    </Document>
    );
    };
    export default App;

    The Outline component only renders content if the PDF contains bookmarks. For PDFs without an outline, it renders nothing.

    Handle errors and loading states

    The Document and Page components accept loading, error, and noData props for displaying a fallback user interface (UI):

    import { useState } from "react";
    import { Document, Page, pdfjs } from "react-pdf";
    import "react-pdf/dist/Page/TextLayer.css";
    import "react-pdf/dist/Page/AnnotationLayer.css";
    pdfjs.GlobalWorkerOptions.workerSrc = new URL(
    "pdfjs-dist/build/pdf.worker.min.mjs",
    import.meta.url,
    ).toString();
    const App = () => {
    const [numPages, setNumPages] = useState(null);
    const [pageNumber, setPageNumber] = useState(1);
    const [loadError, setLoadError] = useState(null);
    const [retryKey, setRetryKey] = useState(0);
    if (loadError) {
    return (
    <div style={{ padding: "20px", color: "red" }}>
    <h2>Failed to load PDF</h2>
    <p>{loadError.message}</p>
    <button
    onClick={() => {
    setLoadError(null);
    setRetryKey((k) => k + 1);
    }}
    >
    Retry
    </button>
    </div>
    );
    }
    return (
    <Document
    key={retryKey}
    file="/document.pdf"
    onLoadSuccess={({ numPages }) => {
    setNumPages(numPages);
    setPageNumber((page) => Math.min(page, numPages));
    }}
    onLoadError={(error) => setLoadError(error)}
    loading={<div style={{ padding: "20px" }}>Loading PDF…</div>}
    noData={<div style={{ padding: "20px" }}>No PDF file specified.</div>}
    >
    <Page
    pageNumber={pageNumber}
    loading={<div style={{ padding: "20px" }}>Rendering page…</div>}
    />
    </Document>
    );
    };
    export default App;

    The onLoadError callback receives the error object. This example stores it in state and shows a retry button. Incrementing retryKey forces React to remount the Document component, which retries the PDF load. The loading prop displays a placeholder while the PDF downloads and parses. noData shows when no file prop is provided.

    TypeScript version

    react-pdf ships its own type definitions. For this version, scaffold a separate project with npm create vite@latest react-pdf-ts -- --template react-ts, install react-pdf@10.5.0, and save the component below as src/PDFViewer.tsx. Put document.pdf in public. Then render <PDFViewer file="/document.pdf" /> from src/App.tsx after importing it. When changing documents, give the component a key based on the document URL so its page and error state reset.

    import { useState } from "react";
    import {
    Document,
    Page,
    Thumbnail,
    Outline,
    pdfjs,
    } from "react-pdf";
    import type { DocumentProps } from "react-pdf";
    import "react-pdf/dist/Page/TextLayer.css";
    import "react-pdf/dist/Page/AnnotationLayer.css";
    pdfjs.GlobalWorkerOptions.workerSrc = new URL(
    "pdfjs-dist/build/pdf.worker.min.mjs",
    import.meta.url,
    ).toString();
    interface PDFViewerProps {
    file: DocumentProps["file"];
    initialPage?: number;
    initialScale?: number;
    }
    const PDFViewer = ({
    file,
    initialPage = 1,
    initialScale = 1.0,
    }: PDFViewerProps) => {
    const [numPages, setNumPages] = useState<number>(0);
    const [pageNumber, setPageNumber] = useState<number>(Number.isInteger(initialPage) && initialPage > 0 ? initialPage : 1);
    const [scale, setScale] = useState<number>(Number.isFinite(initialScale) ? Math.min(3, Math.max(0.5, initialScale)) : 1);
    const [loadError, setLoadError] = useState<Error | null>(null);
    const [retryKey, setRetryKey] = useState<number>(0);
    const goToPrevPage = () => setPageNumber((prev) => Math.max(prev - 1, 1));
    const goToNextPage = () =>
    setPageNumber((prev) => (numPages ? Math.min(prev + 1, numPages) : prev));
    const zoomIn = () => setScale((prev) => Math.min(prev + 0.25, 3));
    const zoomOut = () => setScale((prev) => Math.max(prev - 0.25, 0.5));
    if (loadError) {
    return (
    <div>
    <p>Failed to load: {loadError.message}</p>
    <button
    onClick={() => {
    setLoadError(null);
    setRetryKey((k) => k + 1);
    }}
    >
    Retry
    </button>
    </div>
    );
    }
    return (
    <Document
    key={retryKey}
    file={file}
    onLoadSuccess={({ numPages }) => {
    setNumPages(numPages);
    setPageNumber((page) => Math.min(page, numPages));
    }}
    onLoadError={setLoadError}
    loading={<div>Loading PDF…</div>}
    >
    <div style={{ display: "flex", gap: "16px" }}>
    {/* Thumbnail sidebar. */}
    <aside style={{ width: "150px", overflowY: "auto" }}>
    {Array.from({ length: numPages }, (_, i) => (
    <div
    key={i}
    style={{
    cursor: "pointer",
    border:
    pageNumber === i + 1
    ? "2px solid #0078d4"
    : "2px solid transparent",
    marginBottom: "4px",
    }}
    >
    <Thumbnail pageNumber={i + 1} width={130} onItemClick={({ pageNumber }) => setPageNumber(pageNumber)} />
    </div>
    ))}
    </aside>
    {/* Main content. */}
    <div>
    <nav style={{ display: "flex", gap: "8px", alignItems: "center" }}>
    <button onClick={goToPrevPage} disabled={pageNumber <= 1}>
    Prev
    </button>
    <span>
    Page {pageNumber} of {numPages}
    </span>
    <button onClick={goToNextPage} disabled={!numPages || pageNumber >= numPages}>
    Next
    </button>
    <span>|</span>
    <button onClick={zoomOut} disabled={scale <= 0.5}>
    −
    </button>
    <span>{Math.round(scale * 100)}%</span>
    <button onClick={zoomIn} disabled={scale >= 3}>
    +
    </button>
    </nav>
    <Outline
    onItemClick={({ pageNumber: pg }) => setPageNumber(pg)}
    />
    <Page pageNumber={pageNumber} scale={scale} />
    </div>
    </div>
    </Document>
    );
    };
    export default PDFViewer;

    The following typing details apply:

    • DocumentProps["file"] gives you the exact union type that Document accepts for its file prop (URL string, ArrayBuffer, Uint8Array, or a configuration object).
    • onLoadSuccess receives a PDFDocumentProxy object, but destructuring { numPages } is enough for most use cases.
    • react-pdf also exports PageProps, ThumbnailProps, and OutlineProps if you need to type-check wrapper components.

    Limitations of react-pdf

    react-pdf is a rendering library, not a full PDF editor. Here are its constraints:

    • No annotation authoring UI — The annotation layer displays supported existing annotations, but there’s no built-in toolbar for creating highlights or drawings.
    • Forms need configuration — Set renderForms on Page to enable supported interactive forms. Saving changed values requires additional integration.
    • No digital signatures — No support for signing or verifying signatures.
    • No editing — Text editing, page reordering, and merging aren’t available.
    • No multiformat support — Only PDF files are supported (no DOCX, XLSX, or images).
    • Limited text selection — The text layer works but can be inconsistent with complex layouts.
    • Large document performance — The thumbnail examples render every page. For large documents, render thumbnails only when they enter the visible area and test memory use.

    If your app only needs to display PDFs, these limitations may not matter. If you need a built-in annotation toolbar, document editing, or signing, read on for an alternative.

    Building a PDF viewer with Nutrient Web SDK

    This section shows how to build the same viewer using Nutrient Web SDK. You’ll set up a React project, install the SDK, and render a PDF with a full-featured UI from the first render.

    Create a React project

    Use Vite to scaffold a new React application:

    Terminal window
    npm create vite@latest nutrient-react-example -- --template react
    cd nutrient-react-example

    Install and configure Nutrient

    This example uses Nutrient Web SDK 1.22.0. Install it with 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.

    Render a PDF

    Place a PDF at public/example.pdf. Then replace src/App.jsx with the following. The effect imports the browser SDK after mounting, reports load failures, and unloads its own container during cleanup:

    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%" }} />
    </>
    );
    }

    Start the app:

    Terminal window
    npm run dev

    Because Nutrient is a commercial product, you’ll see an evaluation watermark. To remove it, contact Sales.

    For additional SDK examples, see GitHub(opens in a new tab).

    Available viewer features

    Nutrient provides the following capabilities. Availability depends on your licensed components and deployment; collaboration requires additional setup:

    • Toolbar with zoom, page navigation, search, and layout controls
    • 17+ annotation types (highlights, stamps, shapes, freehand, and comments)
    • PDF form filling and submission
    • Electronic and digital signatures
    • Text editing and page management
    • Multiformat rendering (PDF, DOCX, XLSX, and images)
    • WebAssembly-based rendering optimized for large documents
    • Screen reader-compatible text layers and keyboard navigation

    react-pdf vs. Nutrient: Feature comparison

    Featurereact-pdfNutrient Web SDK
    LicenseMITCommercial
    Prebuilt UINone (build your own)Full toolbar, sidebar, modals
    AnnotationsDisplay existing annotations; no authoring toolbarAnnotation tools and programmatic API
    Form fillingInteractive forms with renderFormsForm filling and creation
    Digital signaturesNot supportedSign and verify
    Text editingNot supportedIn-document editing
    File format supportPDF onlyPDF, DOCX, XLSX, and images
    Rendering engineJavaScript (PDF.js)WebAssembly
    Text selectionBasic text layerAdvanced selection with copy/paste
    Mobile supportManual responsive CSSBuilt-in touch gestures and responsive UI
    AccessibilityManual ARIA workAccessibility features; validate your integration
    SupportCommunity (GitHub issues)Commercial support; terms depend on plan

    When to choose react-pdf

    react-pdf is a good choice when:

    • You need read-only PDF viewing — Display documents without editing or annotation features.
    • You want a lightweight dependency — The MIT-licensed library adds minimal bundle overhead.
    • You need full UI control — You’re building a custom viewer design and want to own every pixel.
    • You’re prototyping — Quick setup for proof-of-concept work before committing to a paid solution.

    When to choose Nutrient

    Nutrient fits better when:

    • You need annotations, forms, or signatures — A prebuilt UI can reduce custom implementation work.
    • You’re rendering large or complex PDFs — Benchmark representative documents against your performance requirements.
    • You need multiformat support — Displaying DOCX, XLSX, or image files alongside PDFs.
    • Accessibility compliance matters — Built-in screen reader support, keyboard navigation, and Web Content Accessibility Guidelines (WCAG) alignment.
    • You want to ship faster — Prebuilt UI components reduce the time from prototype to production.

    Does a React PDF viewer need Document Engine?

    The Nutrient example in this tutorial runs in the browser without Document Engine. Choose the deployment based on the work your application needs to do:

    • Browser viewing and editing — Use Nutrient Web SDK for the viewer UI and the features included in your license. Start with the React and Vite setup guide.
    • A viewer with server processing or collaboration — Combine Web SDK with Document Engine. Check the deployment comparison for the required components and infrastructure.
    • Document processing without a viewer — Consider Nutrient DWS Processor API for hosted conversion, OCR, or other document operations. An API response doesn’t provide a React viewer UI.

    For an existing react-pdf app, follow the migration guide before evaluating additional components in a Nutrient trial.

    Accessibility

    • react-pdf — The text layer enables basic screen reader access. Adding Accessible Rich Internet Applications (ARIA) attributes, keyboard navigation, and focus management is up to you.
    • Nutrient Web SDK — Ships with screen reader-compatible text layers, full keyboard access (tab order, shortcuts, focus trapping), high-contrast modes, and accessible annotation tools. Validate your application against its accessibility requirements.

    Accessibility is hard to retrofit. If your project has compliance requirements, evaluate this early.

    Conclusion

    This tutorial covered building a React PDF viewer with react-pdf — from basic rendering through zoom, thumbnails, outline navigation, error handling, and TypeScript. For read-only viewing with a custom UI, react-pdf works well.

    If you need annotations, forms, signatures, or multiformat support, try Nutrient’s React PDF library. It provides a viewer UI with optional capabilities based on your license.

    This tutorial covers the react-pdf library specifically. For other approaches to viewing documents in React, see:

    FAQ

    How do I migrate from react-pdf to Nutrient?

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

    How do I build a React PDF viewer with react-pdf?

    Install react-pdf (npm install react-pdf@10.5.0), import the Document and Page components, configure the PDF.js worker in the same file, and build your own navigation controls. This tutorial walks through the complete process, including zoom, thumbnails, and outline navigation.

    What are the main limitations of react-pdf?

    react-pdf supplies viewing components without an annotation authoring toolbar, digital signing, or document editing. It displays supported existing annotations and can render interactive forms with renderForms. Large documents need careful thumbnail and page rendering. See the full limitations list.

    Does react-pdf support TypeScript?

    Yes. react-pdf ships type definitions and exports types like DocumentProps, PageProps, ThumbnailProps, and OutlineProps. This tutorial includes a full TypeScript implementation with a typed PDFViewerProps interface.

    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