This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /blog/react-pdf-thumbnails-page-navigation.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. react-pdf thumbnails and page navigation

Table of contents

    This guide shows how to build a thumbnail sidebar and page navigation with react-pdf — using the Thumbnail component for page previews, adding click-to-navigate behavior, and optimizing rendering performance for large documents.
    react-pdf thumbnails and page navigation
    TL;DR

    react-pdf ships a dedicated Thumbnail component for rendering page previews. It’s lighter than Page (no text layer, no annotation layer) and exposes an onItemClick callback for click-to-navigate. The full sidebar pattern has four parts:

    • Wrap everything in <Document> — both Thumbnail and Page need to live inside it.
    • Render one Thumbnail per page with pageNumber, width, and an onItemClick handler that sets your active page state.
    • Render the active page with <Page pageNumber={currentPage} /> in the main area.
    • Cap devicePixelRatio={1} on thumbnails so a 100-page document doesn’t allocate 100× retina-resolution canvases.

    If you’d rather not build this, Nutrient Web SDK has a built-in thumbnail sidebar (SidebarMode.THUMBNAILS) with lazy loading, scroll sync, and page labels. For page reordering, rotation, and deletion, switch to Document Editor mode (InteractionMode.DOCUMENT_EDITOR).

    react-pdf provides a dedicated Thumbnail component for rendering small page previews. Combined with page navigation state, you can build a full sidebar-based PDF viewer.

    The Thumbnail component

    Thumbnail renders a simplified version of a page — no text layer, no annotation layer, just the visual content:

    import { useState } from "react";
    import { Document, Page, Thumbnail } from "react-pdf";
    function PDFWithThumbnails({ file }) {
    const [numPages, setNumPages] = useState(null);
    const [currentPage, setCurrentPage] = useState(1);
    return (
    <Document
    file={file}
    onLoadSuccess={({ numPages }) => setNumPages(numPages)}
    >
    <div style={{ display: "flex" }}>
    <aside className="thumbnail-sidebar">
    {numPages &&
    Array.from({ length: numPages }, (_, i) => (
    <Thumbnail
    key={`thumb-${i + 1}`}
    pageNumber={i + 1}
    width={150}
    onItemClick={({ pageNumber }) => setCurrentPage(pageNumber)}
    className={currentPage === i + 1 ? "active" : ""}
    />
    ))}
    </aside>
    <main>
    <Page pageNumber={currentPage} />
    </main>
    </div>
    </Document>
    );
    }

    Thumbnail props

    Thumbnail shares most props with Page, except it does not support:

    • customTextRenderer
    • renderAnnotationLayer/renderForms/renderTextLayer
    • Text layer callbacks (onGetTextSuccess, onRenderTextLayerSuccess, etc.)
    • Annotation layer callbacks (onGetAnnotationsSuccess, onRenderAnnotationLayerSuccess, etc.)

    Supported Thumbnail props

    PropTypeDescription
    pageNumbernumberPage to thumbnail (1-indexed)
    pageIndexnumberPage to thumbnail (0-indexed)
    widthnumberThumbnail width in pixels
    heightnumberThumbnail height (ignored if width is set)
    scalenumberScale factor
    rotatenumberRotation (0, 90, 180, 270)
    classNamestring/string[]CSS class(es)
    canvasBackgroundstringCanvas background color
    canvasRefrefRef to the canvas element
    inputRefrefRef to the root div
    devicePixelRationumberPixel ratio override
    renderModestring"canvas", "custom", or "none"
    onItemClickfunctionClick handler ({ dest, pageIndex, pageNumber })
    onLoadSuccessfunctionPage data loaded
    onLoadErrorfunctionPage load failed
    onRenderSuccessfunctionCanvas rendered
    onRenderErrorfunctionCanvas render failed

    Thumbnail click handling

    Thumbnail has a built-in onItemClick prop:

    <Thumbnail
    pageNumber={3}
    onItemClick={({ pageNumber, pageIndex }) => {
    setCurrentPage(pageNumber); // 1-indexed.
    }}
    />

    Styling thumbnails

    These styles cover the sidebar layout, thumbnail hover and active states, and the page-number label:

    .thumbnail-sidebar {
    width: 180px;
    height: 100vh;
    overflow-y: auto;
    background: #2a2a2e;
    padding: 8px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    }
    .react-pdf__Thumbnail {
    cursor: pointer;
    border: 2px solid transparent;
    border-radius: 4px;
    transition: border-color 0.15s;
    }
    .react-pdf__Thumbnail:hover {
    border-color: rgba(255, 255, 255, 0.3);
    }
    .react-pdf__Thumbnail.active {
    border-color: #4A90D9;
    }
    /* Add page number labels */
    .thumbnail-wrapper {
    text-align: center;
    }
    .thumbnail-label {
    color: #999;
    font-size: 12px;
    margin-top: 2px;
    }

    Thumbnail with page labels

    Wrap each Thumbnail in a small component that renders the page number underneath it:

    function LabeledThumbnail({ pageNumber, isActive, onClick }) {
    return (
    <div className="thumbnail-wrapper">
    <Thumbnail
    pageNumber={pageNumber}
    width={140}
    onItemClick={onClick}
    className={isActive ? "active" : ""}
    />
    <span className="thumbnail-label">{pageNumber}</span>
    </div>
    );
    }

    This section covers two navigation patterns: paginated controls for jumping between individual pages, and continuous scroll for rendering every page in one scrollable container.

    Simple page controls

    Wire up a previous/next button pair with a page-number input:

    function PageNav({ currentPage, numPages, onPageChange }) {
    return (
    <div className="page-nav">
    <button
    disabled={currentPage <= 1}
    onClick={() => onPageChange(currentPage - 1)}
    >
    Previous
    </button>
    <input
    type="number"
    min={1}
    max={numPages}
    value={currentPage}
    onChange={(e) => {
    const page = parseInt(e.target.value, 10);
    if (page >= 1 && page <= numPages) onPageChange(page);
    }}
    />
    <span>/ {numPages}</span>
    <button
    disabled={currentPage >= numPages}
    onClick={() => onPageChange(currentPage + 1)}
    >
    Next
    </button>
    </div>
    );
    }

    Continuous scroll (all pages)

    Render every page in a scrollable container instead of paginating:

    function ContinuousScroll() {
    const [numPages, setNumPages] = useState(null);
    return (
    <Document
    file={file}
    onLoadSuccess={({ numPages }) => setNumPages(numPages)}
    >
    <div className="scroll-container">
    {numPages &&
    Array.from({ length: numPages }, (_, i) => (
    <Page
    key={`page-${i + 1}`}
    pageNumber={i + 1}
    width={800}
    />
    ))}
    </div>
    </Document>
    );
    }

    Performance: Thumbnail resolution

    For thumbnails, cap the device pixel ratio to reduce memory usage:

    <Thumbnail
    pageNumber={1}
    width={150}
    devicePixelRatio={1} // Render at 1×, not 2×/3×.
    />

    This is especially important when rendering many thumbnails for a large document.

    Key points

    • Thumbnail is a dedicated component — lighter than Page (no text/annotation layers).
    • Use width to control thumbnail size (height scales proportionally).
    • onItemClick provides pageNumber for navigation.
    • Cap devicePixelRatio on thumbnails for better performance.
    • For large documents, consider only rendering visible thumbnails (virtualization).
    • Thumbnail must be inside a Document component.

    FAQ

    Why use Thumbnail instead of just rendering a small Page?

    Thumbnail skips the text layer, annotation layer, and form rendering — it only paints the canvas. PDF.js’s text layer creates one <span> per text item, so skipping it on a text-heavy 100-page document typically saves thousands of DOM nodes and several megabytes of memory. If you render Page at a small width, you still pay for layers nobody can see.

    Why cap devicePixelRatio on thumbnails?

    By default react-pdf paints the canvas at the device’s pixel ratio (typically 2× on retina laptops, 3× on phones). For a 150-pixel-wide thumbnail of a US Letter page at 3×, that’s a ~450×583 canvas — about 1 MB of backing store per page (width × height × 4 bytes for RGBA). A 100-page document allocates ~105 MB just for thumbnail pixels. Setting devicePixelRatio={1} brings each thumbnail down to ~117 KB and the document total to roughly 12 MB.

    How do I virtualize thumbnails for very large documents?

    react-pdf doesn’t include built-in virtualization. Wrap the thumbnail list in react-window or react-virtuoso and only render the slice in view. Each Thumbnail will still trigger its own canvas render, but you’ll have at most ~20 active at a time instead of all numPages.

    Why is onItemClick called instead of just onClick?

    react-pdf reuses the same callback signature for Thumbnail, Outline, and Link annotations — all three pass { dest, pageIndex, pageNumber }. Using onItemClick makes the contract consistent across click sources, so a single navigation handler can drive every clickable surface in the viewer.

    Can I show page labels instead of page numbers?

    PDF documents can carry custom page labels (e.g. Roman numerals for a table of contents, then Arabic numerals for chapters). react-pdf doesn’t expose these through Thumbnail’s callbacks. To get them, call pdfDocument.getPageLabels() on the loaded PDFDocumentProxy and map page index to label in your sidebar component.

    How does Nutrient Web SDK compare?

    Nutrient ships a thumbnail sidebar as a viewer mode — SidebarMode.THUMBNAILS — with lazy loading, scroll sync, and page labels. For page reordering, rotation, and deletion, switch to Document Editor mode (InteractionMode.DOCUMENT_EDITOR), which uses toolbar buttons for those operations. There’s no Thumbnail rendering loop, no manual click wiring, and no virtualization layer. See the migration guide for switching from react-pdf.

    How Nutrient Web SDK handles this

    All the custom thumbnail rendering, DPI capping, and page navigation logic above is built into Nutrient Web SDK:

    // Built-in thumbnail sidebar with lazy loading — one line.
    instance.setViewState((v) =>
    v.set("sidebarMode", NutrientViewer.SidebarMode.THUMBNAILS),
    );
    // Page navigation.
    instance.setViewState((v) => v.set("currentPageIndex", 4));

    Nutrient’s thumbnail sidebar handles lazy loading, scroll sync, active-page highlighting, and page labels — no manual <Thumbnail> rendering or devicePixelRatio tuning. For page reordering, rotation, and deletion, switch to Document Editor mode.


    See Nutrient Web SDK for a built-in thumbnail sidebar, or follow the migration guide to switch from react-pdf. Talk to Sales about your requirements.

    Austin Nguyen

    Austin Nguyen

    AI Engineer

    When Austin isn’t pulling all-nighters to build new features, he enjoys watching science videos on YouTube and cooking.

    Explore related topics

    Try for free Ready to get started?