0

PDF Thumbnail Sidebar

A virtualized column of page thumbnails that tracks and drives the viewer's current page.

"use client";

import {

About

<PdfThumbnailSidebar /> renders a scrollable set of page previews — a rail beside the document or a strip above or below it. Clicking one scrolls the document to that page, and the thumbnail for the page you're on stays marked as you scroll.

Both orientations reflow along the axis they can grow: the vertical rail adds columns as it widens, the horizontal strip adds rows as it grows taller. Each is the other transposed.

Thumbnails are rendered on demand and virtualized — only the ones near the viewport exist in the DOM, so a five-hundred-page document costs about the same as a five-page one.

Installation

pnpm dlx shadcn@latest add pdfcn/thumbnail-sidebar

Usage

import { PdfThumbnailSidebar } from "@/components/pdf-thumbnail-sidebar";
<PdfViewer documents={[{ url: "/sample.pdf" }]} className="h-[600px] flex-row">
  <PdfThumbnailSidebar className="w-40 shrink-0 border-r" />
  <PdfViewerContent />
</PdfViewer>

<PdfViewer /> is a flex column by default, which stacks the sidebar above the document. Override it with flex-row to put them side by side, and give the sidebar a width — it fills its parent's height but has no width of its own. The more width you give it, the more columns the thumbnails flow into; drop it in a resizable panel, as the demo does, to let the reader drive that.

The thumbnails keep a fixed size — widen the rail and it fits more columns rather than growing the cards. Set that size, and the gutter and padding around it, with the thumbnailSize, gap, and padding props.

The thumbnail plugin is part of the light tier <PdfViewer /> already registers, so nothing needs to be added to plugins.

Orientation

Set orientation="horizontal" for a filmstrip instead of a column. It fills its parent's width and needs a height rather than a width, so keep <PdfViewer /> a flex column and give the strip a height. The taller you make it, the more rows the thumbnails flow into — the vertical rail transposed — and it scrolls sideways through them. Drop it in a resizable panel, as the horizontal example does, to let the reader drive that:

<PdfViewer documents={[{ url: "/sample.pdf" }]} className="h-[600px]">
  <PdfViewerContent />
  <PdfThumbnailSidebar
    orientation="horizontal"
    className="h-40 shrink-0 border-t"
  />
</PdfViewer>

Sizing

Both orientations lay out their own cards, so the size and spacing come from the component rather than from CSS or the plugin. Each card has one fixed dimension and lets the other follow the page's proportions: the vertical rail fixes the card width (120px by default) and fits as many columns as its width allows; the horizontal strip fixes the height (96px) and fits as many rows as its height allows. Set these with props rather than on the sidebar's box:

PropControls
thumbnailSizeThe card's fixed dimension — its width when vertical, height when horizontal.
gapThe gutter between thumbnails.
paddingThe padding around the whole rail or strip.
<PdfThumbnailSidebar thumbnailSize={140} gap={16} padding={16} />

The plugin's width is a separate setting — it controls the resolution each thumbnail bitmap is rendered at, not its on-screen size. <PdfViewer /> registers it to match the card, so thumbnails are crisp without over-rendering. Raise it at the registration site if you raise thumbnailSize:

createPluginRegistration(ThumbnailPluginPackage, { width: 160 });

See embedpdf's Thumbnail plugin for the rest of its options.

Examples

Default

The rail sits in a resizable panel — drag the handle to widen it and watch the thumbnails reflow from one column into two or three.

"use client";

import {

Horizontal

The strip sits in a resizable panel too — drag the handle up to make it taller and the thumbnails flow from one row into two or three.

"use client";

import {

Accessibility

Every thumbnail is a real <button> labelled Page {n}, so the column is navigable with Tab and each target is announced by number rather than by an unlabelled image. The button for the page currently in view carries aria-current="page", and is marked visually with a ring and a bolder label — state that isn't carried by colour alone.

Thumbnails are decorative duplicates of content already in the viewport, so the images themselves add nothing for a screen reader; the button label is what identifies each row.

API Reference

PdfThumbnailSidebar

The scroll container, carrying data-slot="pdf-thumbnail-sidebar" and data-orientation. Accepts every div prop, plus:

PropTypeDefaultDescription
orientation"vertical" | "horizontal""vertical"A column beside the document, or a strip above or below it.
thumbnailSizenumber120 / 96The card's fixed dimension in pixels — width when vertical, height when horizontal. Defaults to 120 vertical, 96 horizontal.
gapnumber12The gutter between thumbnails, in pixels.
paddingnumber12The padding around the whole rail or strip, in pixels.

Reads the active document from <PdfViewer />'s context and shows placeholder cards until it loads, keeping the sidebar at a stable size throughout.

Current-page tracking runs through the scroll plugin in both directions: the sidebar reads currentPage to mark the active thumbnail, and calls scrollToPage when one is clicked. It also scrolls itself to keep the active thumbnail in view as the document moves.

Both orientations are virtualized by hand — only thumbnails near the viewport are in the DOM. embedpdf's ThumbnailsPane lays out only a single fixed column, so a ResizeObserver measures the container and one set of arithmetic drives both layouts, each the other transposed, reusing the plugin's ThumbImg to render each bitmap.

Styling the thumbnails

SlotElementDescription
pdf-thumbnail-sidebardivThe scroll container.
pdf-thumbnailbuttonOne page. Carries data-active when it's the current page.

In both orientations, cards are positioned absolutely from the virtualizer's own layout, so their offsets and sizes aren't yours to set — style the button's contents rather than its box.