# Thumbnails

Add a scrollable page-preview sidebar beside the document.

Source: https://anara.com/lector/docs/code/thumbnails

These docs describe the source deployed with this site, tracking main rather than a versioned npm release. Check your installed @anaralabs/lector and pdfjs-dist versions before applying examples.

`Thumbnails` clones one `Thumbnail` template for every page. Each thumbnail navigates to its page on click or Enter. Both thumbnails and pages must share the same `Root`.

## A sidebar layout

Complete [installation](https://anara.com/lector/docs/installation) and create `pdf-setup.ts` before using this example.

```tsx title="thumbnail-viewer.tsx"
"use client";

import {
  CanvasLayer, Page, Pages, Root, TextLayer, Thumbnail, Thumbnails,
} from "@anaralabs/lector";
import "./pdf-setup";

export default function ThumbnailViewer() {
  return (
    <Root
      source="/sample.pdf"
      isZoomFitWidth
      style={{ height: 600, display: "flex" }}
    >
      <aside aria-label="Page thumbnails" style={{ width: 160, flexShrink: 0, overflow: "auto" }}>
        <Thumbnails style={{ display: "grid", gap: 16, padding: 8 }}>
          <Thumbnail style={{ width: 128 }} aria-label="Go to this page" />
        </Thumbnails>
      </aside>
      <div style={{ flex: 1, minWidth: 0 }}>
        <Pages><Page><CanvasLayer /><TextLayer /></Page></Pages>
      </div>
    </Root>
  );
}
```

The sidebar scrolls independently. `minWidth: 0` allows the main viewer to shrink when the sidebar takes space. Set a CSS width on `Thumbnail`; Lector owns the canvas's drawing dimensions.

## Label individual pages

For specific labels and an active-page indicator, map thumbnails yourself inside a child of `Root`:

```tsx title="labeled-thumbnails.tsx"
"use client";

import { Thumbnail, usePdf } from "@anaralabs/lector";

export default function LabeledThumbnails() {
  const count = usePdf((state) => state.pdfDocumentProxy.numPages);
  const current = usePdf((state) => state.currentPage);

  return (
    <div style={{ display: "grid", gap: 16 }}>
      {Array.from({ length: count }, (_, index) => {
        const page = index + 1;
        return (
          <div key={page}>
            <Thumbnail
              pageNumber={page}
              aria-label={`Go to page ${page}`}
              aria-current={current === page ? "page" : undefined}
              style={{ width: 128, outline: current === page ? "2px solid currentColor" : undefined }}
            />
            <span>Page {page}</span>
          </div>
        );
      })}
    </div>
  );
}
```

Keep a visible focus indicator in your app's styles. The built-in keyboard handler supports Enter; if your UI needs Space activation too, provide an `onKeyDown` handler that prevents the page scroll and activates the thumbnail.

`Thumbnails` creates a wrapper for every page; it is not the same virtualized list as `Pages`. Thumbnail canvas rendering is visibility-driven. For very large documents, measure the sidebar separately and consider mounting it only when opened.

## Live example

Open the documentation page linked above to use this interactive example.

## Virtual thumbnails for long documents

For hundreds or thousands of pages, opt into a bounded list. Give `Thumbnails`
a bounded height and specify the fixed height of each row, including spacing:

```tsx
<Thumbnails
  style={{ height: 450, width: 140 }}
  virtualize={{ itemHeight: 150, overscan: 2 }}
>
  <Thumbnail style={{ height: 130, width: "auto" }} />
</Thumbnails>
```

The container owns scrolling and mounts only nearby rows. Ensure each child's
content fits `itemHeight`; margins, padding and spacing belong inside that row.
The default layout remains available by omitting `virtualize`.

With a thumbnail focused, Arrow Up/Down moves between pages, and Home/End moves
to the first/last page, mounting the target before focusing it. Enter opens the
focused page. Consumer keyboard handlers can call `preventDefault()` to override
the virtual list's navigation keys.
