---
title: "How to build a thumbnail sidebar with PDF.js PDFThumbnailViewer"
canonical_url: "https://www.nutrient.io/blog/pdfjs-thumbnail-sidebar/"
md_url: "https://www.nutrient.io/blog/pdfjs-thumbnail-sidebar.md"
last_updated: "2026-07-24T20:39:08.894Z"
description: "Add a page thumbnail sidebar to PDF.js using the built-in `PDFThumbnailViewer` or custom canvas rendering with lazy loading and render cancellation."
---

**TL;DR**

- Use PDF.js’s built-in `PDFThumbnailViewer` (from `pdfjs-dist/web/pdf_viewer.mjs`) for a sidebar with rendering, caching, and click navigation handled for you — share its `PDFRenderingQueue` with the main viewer.

- For a custom UI, render pages to small canvases via `page.render()` at a low `scale` (0.15–0.25). Then convert with `canvas.toDataURL()`.

- For large documents, lazy-load thumbnails with `IntersectionObserver` and call `renderTask.cancel()` on fast scroll to avoid wasted work.

- Nutrient Web SDK ships a thumbnail sidebar via `SidebarMode.THUMBNAILS` — one line of configuration replaces the entire pipeline above.

PDF.js includes a built-in `PDFThumbnailViewer` for rendering page thumbnails in a sidebar. You can also build thumbnails manually using the lower-level `page.render()` API for more control.

## Option 1: Built-in PDFThumbnailViewer

The viewer layer includes a thumbnail viewer that handles rendering, caching, and scroll synchronization. It needs a shared `PDFRenderingQueue` so thumbnail renders coordinate with main-viewer renders:

```js

const pdfjs = await import("pdfjs-dist/web/pdf_viewer.mjs");

const thumbnailContainer = document.getElementById("thumbnail-container");

const thumbnailViewer = new pdfjs.PDFThumbnailViewer({
  container: thumbnailContainer,
  eventBus,
  linkService,
  renderingQueue,
});

// After document loads.
thumbnailViewer.setDocument(pdfDocument);

```

### HTML structure

`PDFThumbnailViewer` renders into a container element you provide:

```html

<div id="thumbnail-container" class="thumbnailContainer">
  <!-- `PDFThumbnailViewer` populates this -->

</div>

```

### CSS

Style the container and each thumbnail so the sidebar scrolls independently and highlights the selected page:

```css.thumbnailContainer {
  width: 200px;
  height: 100%;
  overflow-y: auto;
  background: #2a2a2e;

  padding: 8px;
}.thumbnail {
  margin: 4px auto;
  cursor: pointer;
  border: 2px solid transparent;
  border-radius: 4px;
}.thumbnail.selected {
  border-color: #4A90D9;

}.thumbnailImage {
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}

```

### Synchronizing with the main viewer

The thumbnail viewer automatically highlights the current page when you listen to page changes:

```js

eventBus.on("pagechanging", (evt) => {
  thumbnailViewer.scrollThumbnailIntoView(evt.pageNumber);
});

```

Clicking a thumbnail navigates to that page (handled automatically via `linkService`).

## Option 2: Custom thumbnails with page.render()

For more control over thumbnail appearance, render pages to small canvases:

```js

async function renderThumbnail(pdfDocument, pageNumber, scale = 0.2) {
  const page = await pdfDocument.getPage(pageNumber);
  const viewport = page.getViewport({ scale });

  const canvas = document.createElement("canvas");
  canvas.width = viewport.width;
  canvas.height = viewport.height;

  const context = canvas.getContext("2d");
  await page.render({
    canvasContext: context,
    viewport,
  }).promise;

  return canvas;
}

```

### React thumbnail grid

Wire `renderThumbnail()` into a component that renders every page as a clickable thumbnail and tracks the current page:

```tsx

function ThumbnailSidebar({ pdfDocument, viewer }) {
  const [thumbnails, setThumbnails] = useState([]);
  const [currentPage, setCurrentPage] = useState(1);

  useEffect(() => {
    async function generateThumbnails() {
      const thumbs = [];
      for (let i = 1; i <= pdfDocument.numPages; i++) {
        const canvas = await renderThumbnail(pdfDocument, i, 0.2);
        thumbs.push({ page: i, dataUrl: canvas.toDataURL() });
      }
      setThumbnails(thumbs);
    }
    generateThumbnails();
  }, [pdfDocument]);

  return (
    <div className="thumbnail-sidebar">
      {thumbnails.map(({ page, dataUrl }) => (
        <button
          key={page}
          type="button"
          className={`thumbnail ${page === currentPage? "selected" : ""}`}
          onClick={() => {
            viewer.currentPageNumber = page;
          }}
        >
          <img src={dataUrl} alt={`Page ${page}`} />
          <span>{page}</span>
        </button>
      ))}
    </div>
  );
}

```

### Lazy loading thumbnails

For large documents, generate thumbnails on demand using an `IntersectionObserver`:

```tsx

function LazyThumbnail({ pdfDocument, pageNumber, onClick, isSelected }) {
  const [dataUrl, setDataUrl] = useState(null);
  const ref = useRef(null);
  const rendered = useRef(false);

  useEffect(() => {
    if (rendered.current) return;
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          rendered.current = true;
          renderThumbnail(pdfDocument, pageNumber, 0.2).then((canvas) => {
            setDataUrl(canvas.toDataURL());
          });
          observer.disconnect();
        }
      },
      { threshold: 0.1 },
    );

    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
  }, [pdfDocument, pageNumber]);

  return (
    <button
      ref={ref}
      type="button"
      className={`thumbnail ${isSelected? "selected" : ""}`}
      onClick={onClick}
      style={{ minHeight: 150 }}
    >
      {dataUrl? (
        <img src={dataUrl} alt={`Page ${pageNumber}`} />
      ) : (
        <div className="thumbnail-placeholder" />
      )}
      <span>{pageNumber}</span>
    </button>
  );
}

```

## Canceling renders

For fast scrolling, cancel pending thumbnail renders. The `currentRenderTask` below is module-scoped for clarity — scope it per sidebar instance (e.g. a ref) if you mount more than one viewer:

```js

let currentRenderTask = null;

async function renderThumbnailCancellable(page, scale) {
  const viewport = page.getViewport({ scale });
  const canvas = document.createElement("canvas");
  canvas.width = viewport.width;
  canvas.height = viewport.height;

  // Cancel previous render if still running.
  if (currentRenderTask) {
    currentRenderTask.cancel();
  }

  const renderTask = page.render({
    canvasContext: canvas.getContext("2d"),
    viewport,
  });
  currentRenderTask = renderTask;

  try {
    await renderTask.promise;
    return canvas;
  } catch (error) {
    if (error.name === "RenderingCancelledException") {
      return null; // Canceled, ignore.
    }
    throw error;
  }
}

```

## Key points

- `PDFThumbnailViewer` is the easiest option — it handles rendering, caching, and click navigation.

- For a custom UI, use `page.render()` with a small viewport `scale` (0.15–0.25).

- Use `canvas.toDataURL()` to convert thumbnails to displayable images.

- Lazy-load with `IntersectionObserver` for large documents.

- `renderTask.cancel()` prevents wasted work when scrolling fast through thumbnails.

- The `PDFThumbnailViewer` synchronizes with the main viewer via `EventBus` automatically.

## How Nutrient Web SDK handles this

Instead of setting up `PDFThumbnailViewer`, writing custom canvas rendering, and managing `IntersectionObserver` lazy loading, Nutrient provides a built-in thumbnail sidebar with a single line of code:

```js

// Enable the built-in thumbnail sidebar — one line.
instance.setViewState((v) =>
  v.set("sidebarMode", NutrientViewer.SidebarMode.THUMBNAILS),
);

```

There’s no `PDFThumbnailViewer` setup, no custom canvas rendering, no `IntersectionObserver` lazy loading, and no render cancellation logic. Nutrient’s built-in thumbnail sidebar handles rendering, lazy loading, scroll synchronization, and page navigation automatically. For page reordering, rotation, duplication, and deletion, switch to [Document Editor mode](https://www.nutrient.io/guides/web/features/document-editor.md) (`InteractionMode.DOCUMENT_EDITOR`).

[Learn more about Nutrient Web SDK](https://www.nutrient.io/sdk/web-overview/) | [Migration guide](https://www.nutrient.io/guides/web/about/migration-guides/migrating-from-mozilla-pdfjs.md) | [Contact Sales](https://www.nutrient.io/contact-sales/?=sdk)

## FAQ

#### Does PDF.js include a thumbnail sidebar out of the box?

Yes — the viewer layer (`pdfjs-dist/web/pdf_viewer.mjs`) ships `PDFThumbnailViewer`. Its constructor needs `container`, `eventBus`, `linkService`, and `renderingQueue`. Share that `renderingQueue` with the main `PDFViewer` so thumbnail and page renders coordinate.

#### When should I build custom thumbnails instead of using <code>PDFThumbnailViewer</code>?

Reach for `page.render()` when you need a layout the built-in component doesn’t support (grid view, embedded inside a custom React tree, virtualized list, etc.) or when you want different styling than the public CSS classes allow. Otherwise, `PDFThumbnailViewer` is the lower-effort path.

#### How do I lazy-load thumbnails for large documents?

Wrap each thumbnail in an `IntersectionObserver` and only call `page.render()` when the placeholder scrolls into view. Track a `rendered` ref so the observer doesn’t fire repeatedly, and disconnect after the first intersection.

#### How do I cancel a thumbnail render mid-flight?

`page.render()` returns a `RenderTask` with a `cancel()` method. Store the latest task, call `cancel()` before starting a new one, and catch `RenderingCancelledException` in the `await` so a canceled render doesn’t surface as an error.

#### Can I reorder pages from the PDF.js thumbnail sidebar?

No — `PDFThumbnailViewer` is navigation-only. Page reordering, rotation, insertion, and deletion aren’t part of the open source viewer. That requires a custom UI plus a library like `pdf-lib` to persist changes, or an SDK such as [Nutrient Web SDK](https://www.nutrient.io/sdk/web-overview/), whose Document Editor mode provides those operations via toolbar buttons.

#### What scale should I use for thumbnails?

0.15–0.25 is the usual range. Lower scales render faster and use less memory but get blurry on screens with a high device pixel ratio (DPR). If you want crisp thumbnails on Retina displays, multiply your chosen scale by `window.devicePixelRatio` and downscale the rendered canvas with CSS.
---

## Related pages

- [The business case for accessibility: Five ways it drives enterprise value](/blog/5-ways-accessibility-drives-enterprise-value.md)
- [Accessibility Untangled Why It Matters Guide](/blog/accessibility-untangled-why-it-matters-guide.md)
- [Advanced Techniques For React Native Ui Components](/blog/advanced-techniques-for-react-native-ui-components.md)
- [`vector_store` holds your indexed documents (see the multimodal RAG post](/blog/agentic-rag.md)
- [Ai Document Automation Extraction To Action](/blog/ai-document-automation-extraction-to-action.md)
- [Ai Legal Assistant Document Authoring](/blog/ai-legal-assistant-document-authoring.md)
- [Angular File Viewer Pdf Image Office Files](/blog/angular-file-viewer-pdf-image-office-files.md)
- [Auto Tagging And Document Accessibility In Dotnet Sdk](/blog/auto-tagging-and-document-accessibility-in-dotnet-sdk.md)
- [Best Document Viewers](/blog/best-document-viewers.md)
- [The CEO’s AI playbook: Why decision architecture beats model selection](/blog/ceo-ai-playbook-decision-architecture.md)
- [1. Extract and chunk the PDF.](/blog/chat-with-pdf.md)
- [Complete Guide To Pdfjs](/blog/complete-guide-to-pdfjs.md)
- [Construction Document Data Extraction](/blog/construction-document-data-extraction.md)
- [Convert One Drive Files To Pdf In Sharepoint](/blog/convert-one-drive-files-to-pdf-in-sharepoint.md)
- [Create And Edit Pdfs In Flutter](/blog/create-and-edit-pdfs-in-flutter.md)
- [Create Pdfs With React](/blog/create-pdfs-with-react.md)
- [Creating A Document Scanner With Ocr In Python](/blog/creating-a-document-scanner-with-ocr-in-python.md)
- [Creating And Filling Pdf Forms Programmatically In Javascript](/blog/creating-and-filling-pdf-forms-programmatically-in-javascript.md)
- [The CTO’s AI playbook: Why accountability architecture beats orchestration](/blog/cto-ai-playbook-accountability-architecture.md)
- [Digital Signatures](/blog/digital-signatures.md)
- [Digital Workflow Automation](/blog/digital-workflow-automation.md)
- [Document Ai Vs Ocr](/blog/document-ai-vs-ocr.md)
- [Document Viewer](/blog/document-viewer.md)
- [Document Watermarking](/blog/document-watermarking.md)
- [Emerging threats: Your logging system may be an agentic threat vector](/blog/emerging-threats-your-logging-system.md)
- [app.py](/blog/extract-text-from-pdf-using-python.md)
- [How To Add Digital Signature To Pdf Using React](/blog/how-to-add-digital-signature-to-pdf-using-react.md)
- [How To Build A Dotnet Maui Pdf Viewer](/blog/how-to-build-a-dotnet-maui-pdf-viewer.md)
- [How To Build A Flutter Pdf Viewer](/blog/how-to-build-a-flutter-pdf-viewer.md)
- [or](/blog/how-to-build-a-javascript-pdf-viewer-with-pdfjs.md)
- [How To Build A Javascript Pdf Viewer](/blog/how-to-build-a-javascript-pdf-viewer.md)
- [or](/blog/how-to-build-a-nextjs-pdf-viewer.md)
- [How To Build A Powerpoint Viewer Using Javascript](/blog/how-to-build-a-powerpoint-viewer-using-javascript.md)
- [Using Yarn](/blog/how-to-build-a-react-excel-viewer.md)
- [How To Build A React Native Pdf Viewer](/blog/how-to-build-a-react-native-pdf-viewer.md)
- [How To Build A React Powerpoint Viewer](/blog/how-to-build-a-react-powerpoint-viewer.md)
- [How To Build A Reactjs File Viewer](/blog/how-to-build-a-reactjs-file-viewer.md)
- [or](/blog/how-to-build-a-reactjs-pdf-viewer-with-react-pdf.md)
- [or](/blog/how-to-build-a-reactjs-pdf-viewer.md)
- [How To Build A Reactjs Viewer With Pdfjs](/blog/how-to-build-a-reactjs-viewer-with-pdfjs.md)
- [How To Build A Vuejs Pdf Viewer With Pdfjs](/blog/how-to-build-a-vuejs-pdf-viewer-with-pdfjs.md)
- [How To Build A Vuejs Pdf Viewer](/blog/how-to-build-a-vuejs-pdf-viewer.md)
- [How To Build An Android Pdf Viewer](/blog/how-to-build-an-android-pdf-viewer.md)
- [How To Build An Angular Pdf Viewer With Ng2 Pdf Viewer](/blog/how-to-build-an-angular-pdf-viewer-with-ng2-pdf-viewer.md)
- [How To Build An Angular Pdf Viewer With Pdfjs](/blog/how-to-build-an-angular-pdf-viewer-with-pdfjs.md)
- [How To Convert Docx To Pdf Using Javascript](/blog/how-to-convert-docx-to-pdf-using-javascript.md)
- [How To Convert Docx To Pdf Using Python](/blog/how-to-convert-docx-to-pdf-using-python.md)
- [How To Convert Html To Pdf Using Html2pdf](/blog/how-to-convert-html-to-pdf-using-html2pdf.md)
- [or](/blog/how-to-convert-html-to-pdf-using-react.md)
- [or](/blog/how-to-convert-html-to-pdf-using-wkhtmltopdf-and-python.md)
- [How To Convert Word To Pdf In Nodejs](/blog/how-to-convert-word-to-pdf-in-nodejs.md)
- [or](/blog/how-to-create-a-react-js-signature-pad.md)
- [How To Create Pdfs With React To Pdf](/blog/how-to-create-pdfs-with-react-to-pdf.md)
- [How To Edit Pdfs Using Ios Pdf Library](/blog/how-to-edit-pdfs-using-ios-pdf-library.md)
- [How To Embed A Pdf Viewer In Your Website](/blog/how-to-embed-a-pdf-viewer-in-your-website.md)
- [How To Generate Pdf From Html With Nodejs](/blog/how-to-generate-pdf-from-html-with-nodejs.md)
- [base_url tells WeasyPrint where to resolve relative asset paths](/blog/how-to-generate-pdf-reports-from-html-in-python.md)
- [Open an image.](/blog/how-to-use-tesseract-ocr-in-python.md)
- [From an HTML string.](/blog/html-in-pdf-format.md)
- [Javascript Pdf Libraries](/blog/javascript-pdf-libraries.md)
- [Linearized Pdf](/blog/linearized-pdf.md)
- [Swift Package Manager](/blog/mobile-pdf-sdk.md)
- [`elements` come from your document parser — each has a type and content.](/blog/multimodal-rag.md)
- [Nutrient Vs Conga Composer](/blog/nutrient-vs-conga-composer.md)
- [Online Document Viewer](/blog/online-document-viewer.md)
- [Open Pdf In Your Web App](/blog/open-pdf-in-your-web-app.md)
- [Pdf Extraction Benchmark Opendataloader Bench](/blog/pdf-extraction-benchmark-opendataloader-bench.md)
- [Pdf Page Labels](/blog/pdf-page-labels.md)
- [Pdf Sdk Compliance Security Checklist](/blog/pdf-sdk-compliance-security-checklist.md)
- [Pdf Sdk Performance Benchmark](/blog/pdf-sdk-performance-benchmark.md)
- [Pdf Ua Compliance Guide](/blog/pdf-ua-compliance-guide.md)
- [Pdfjs Annotation Editor Layer](/blog/pdfjs-annotation-editor-layer.md)
- [Pdfjs Area Annotations Canvas Capture](/blog/pdfjs-area-annotations-canvas-capture.md)
- [Pdfjs Coordinate Systems Pdf To Screen](/blog/pdfjs-coordinate-systems-pdf-to-screen.md)
- [Pdfjs Eventbus Guide](/blog/pdfjs-eventbus-guide.md)
- [Pdfjs Limitations Commercial Upgrade](/blog/pdfjs-limitations-commercial-upgrade.md)
- [Pdfjs Native Annotation Layer Forms](/blog/pdfjs-native-annotation-layer-forms.md)
- [Pdfjs Navigation Zoom Rotation](/blog/pdfjs-navigation-zoom-rotation.md)
- [Pdfjs Pdf Page Manipulation Pdf Lib](/blog/pdfjs-pdf-page-manipulation-pdf-lib.md)
- [Pdfjs React Viewer Setup](/blog/pdfjs-react-viewer-setup.md)
- [Pdfjs Rendering Overlays React Portals](/blog/pdfjs-rendering-overlays-react-portals.md)
- [Pdfjs Server Side Text Extraction](/blog/pdfjs-server-side-text-extraction.md)
- [Pdfjs Sticky Note Annotations](/blog/pdfjs-sticky-note-annotations.md)
- [Pdfjs Text Highlight Annotations](/blog/pdfjs-text-highlight-annotations.md)
- [Pdfjs Text Search Pdffindcontroller](/blog/pdfjs-text-search-pdffindcontroller.md)
- [Process Flows](/blog/process-flows.md)
- [React Native Pdf Annotation](/blog/react-native-pdf-annotation.md)
- [or](/blog/sample-blog-updated.md)
- [Add DWS MCP Server to your Claude Code project.](/blog/teaching-llms-to-read-pdfs.md)
- [Open an image file.](/blog/tesseract-python-guide.md)
- [Top 5 Javascript Pdf Viewers](/blog/top-5-javascript-pdf-viewers.md)
- [Convert an HTML file to PDF.](/blog/top-ten-ways-to-convert-html-to-pdf.md)
- [Vector Pdf](/blog/vector-pdf.md)
- [Wcag2 Accessibility Requirements Documents](/blog/wcag2-accessibility-requirements-documents.md)
- [Web Sdk Is Now Headless](/blog/web-sdk-is-now-headless.md)
- [What Are Annotations](/blog/what-are-annotations.md)
- [What Is A Vpat](/blog/what-is-a-vpat.md)
- [What Is Pdf Ua](/blog/what-is-pdf-ua.md)
- [Why Pdfium Is A Trusted Platform For Pdf Rendering](/blog/why-pdfium-is-a-trusted-platform-for-pdf-rendering.md)
- [Why Your Ai Agent Hallucinates Pdf Table Data](/blog/why-your-ai-agent-hallucinates-pdf-table-data.md)

