Select a Thumbnail Page to Add a Custom Action
Scenario
Section titled “Scenario”When the thumbnail sidebar is displayed, users can select pages directly from the thumbnail list.
This is useful for document workflows where selected pages are sent to another action, such as:
- Extracting selected pages
- Removing pages from a document
- Sending chosen pages to a backend process
- Building a custom page-based review or approval flow
In this example, users can:
- Select all pages
- Deselect all pages
- Select or deselect individual pages from the thumbnail sidebar
What to Use
Section titled “What to Use”The useElementThumbnailContext hook provides the updateElement function to add custom elements to a specific thumbnail page.
| Name | Objective |
|---|---|
updateElement | Add a custom checkbox overlay to each thumbnail page |
clearElements | Remove thumbnail overlay elements when the component is cleaned up |
The usePaginationContext hook provides the totalPages value so the example can render one custom checkbox for every page in the document.
| Name | Objective |
|---|---|
totalPages | Determine how many thumbnail checkboxes should be rendered |
import { RPConfig, RPProvider, RPLayout, RPPages, useElementThumbnailContext, usePaginationContext,} from "@react-pdf-kit/viewer";import { useCallback, useEffect, useMemo, useState } from "react";
const buttonStyle = { border: "1px solid #cbd5e1", borderRadius: 4, background: "#ffffff", padding: "6px 10px", cursor: "pointer",};
const toolbarStyle = { display: "flex", alignItems: "center", flexWrap: "wrap", gap: 8, padding: 8, borderBottom: "1px solid #e2e8f0",};
const ThumbnailPageSelector = () => { const { updateElement, clearElements } = useElementThumbnailContext(); const { totalPages } = usePaginationContext(); const [selectedPages, setSelectedPages] = useState([]);
// Convert the selected page array into a Set for quick lookup while rendering each thumbnail. const selectedPageSet = useMemo(() => new Set(selectedPages), [selectedPages]);
const togglePage = useCallback((page) => { setSelectedPages((prev) => prev.includes(page) ? prev.filter((item) => item !== page) : [...prev, page] ); }, []);
// totalPages is 1-based in the viewer API, so create page numbers from 1 to totalPages. const selectAllPages = useCallback(() => { setSelectedPages(Array.from({ length: totalPages }, (_, index) => index + 1)); }, [totalPages]);
const deselectAllPages = useCallback(() => { setSelectedPages([]); }, []);
useEffect(() => { if (!totalPages) { return; }
// Inject one checkbox overlay into each thumbnail page. for (let page = 1; page <= totalPages; page++) { const isSelected = selectedPageSet.has(page);
updateElement(page, () => [ <label key={`thumbnail-page-selector-${page}`} onClick={(event) => event.stopPropagation()} style={{ position: "absolute", top: 6, right: 6, display: "inline-flex", alignItems: "center", justifyContent: "center", width: 24, height: 24, borderRadius: 4, backgroundColor: isSelected ? "#2563eb" : "#ffffff", border: `2px solid ${isSelected ? "#1d4ed8" : "#94a3b8"}`, boxShadow: "0 1px 4px rgba(15, 23, 42, 0.25)", cursor: "pointer", zIndex: 2, }} > <input type="checkbox" aria-label={`Select page ${page}`} checked={isSelected} onChange={() => togglePage(page)} onClick={(event) => event.stopPropagation()} style={{ width: 14, height: 14, margin: 0, accentColor: "#2563eb", cursor: "pointer", }} /> </label>, ]); }
// Remove injected thumbnail controls when the document changes or the component unmounts. return () => { for (let page = 1; page <= totalPages; page++) { clearElements(page); } }; }, [clearElements, selectedPageSet, togglePage, totalPages, updateElement]);
const selectedPagesLabel = selectedPages.length ? [...selectedPages].sort((a, b) => a - b).join(", ") : "(none)";
return ( <div style={toolbarStyle}> <button type="button" onClick={selectAllPages} style={buttonStyle}> Select all pages </button> <button type="button" onClick={deselectAllPages} style={buttonStyle}> Deselect all pages </button> <span>Selected pages: {selectedPagesLabel}</span> </div> );};
export const AppPdfViewer = () => { return ( <RPConfig licenseKey="YOUR_DOMAIN_TOKEN"> <RPProvider src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"> <ThumbnailPageSelector /> <RPLayout toolbar> <RPPages /> </RPLayout> </RPProvider> </RPConfig> );};import { RPConfig, RPProvider, RPLayout, RPPages, useElementThumbnailContext, usePaginationContext,} from "@react-pdf-kit/viewer";import { useCallback, useEffect, useMemo, useState, type CSSProperties, type FC } from "react";
const buttonStyle: CSSProperties = { border: "1px solid #cbd5e1", borderRadius: 4, background: "#ffffff", padding: "6px 10px", cursor: "pointer",};
const toolbarStyle: CSSProperties = { display: "flex", alignItems: "center", flexWrap: "wrap", gap: 8, padding: 8, borderBottom: "1px solid #e2e8f0",};
const ThumbnailPageSelector: FC = () => { const { updateElement, clearElements } = useElementThumbnailContext(); const { totalPages } = usePaginationContext(); const [selectedPages, setSelectedPages] = useState<number[]>([]);
// Convert the selected page array into a Set for quick lookup while rendering each thumbnail. const selectedPageSet = useMemo(() => new Set(selectedPages), [selectedPages]);
const togglePage = useCallback((page: number) => { setSelectedPages((prev) => prev.includes(page) ? prev.filter((item) => item !== page) : [...prev, page] ); }, []);
// totalPages is 1-based in the viewer API, so create page numbers from 1 to totalPages. const selectAllPages = useCallback(() => { setSelectedPages(Array.from({ length: totalPages }, (_, index) => index + 1)); }, [totalPages]);
const deselectAllPages = useCallback(() => { setSelectedPages([]); }, []);
useEffect(() => { if (!totalPages) { return; }
// Inject one checkbox overlay into each thumbnail page. for (let page = 1; page <= totalPages; page++) { const isSelected = selectedPageSet.has(page);
updateElement(page, () => [ <label key={`thumbnail-page-selector-${page}`} onClick={(event) => event.stopPropagation()} style={{ position: "absolute", top: 6, right: 6, display: "inline-flex", alignItems: "center", justifyContent: "center", width: 24, height: 24, borderRadius: 4, backgroundColor: isSelected ? "#2563eb" : "#ffffff", border: `2px solid ${isSelected ? "#1d4ed8" : "#94a3b8"}`, boxShadow: "0 1px 4px rgba(15, 23, 42, 0.25)", cursor: "pointer", zIndex: 2, }} > <input type="checkbox" aria-label={`Select page ${page}`} checked={isSelected} onChange={() => togglePage(page)} onClick={(event) => event.stopPropagation()} style={{ width: 14, height: 14, margin: 0, accentColor: "#2563eb", cursor: "pointer", }} /> </label>, ]); }
// Remove injected thumbnail controls when the document changes or the component unmounts. return () => { for (let page = 1; page <= totalPages; page++) { clearElements(page); } }; }, [clearElements, selectedPageSet, togglePage, totalPages, updateElement]);
const selectedPagesLabel = selectedPages.length ? [...selectedPages].sort((a, b) => a - b).join(", ") : "(none)";
return ( <div style={toolbarStyle}> <button type="button" onClick={selectAllPages} style={buttonStyle}> Select all pages </button> <button type="button" onClick={deselectAllPages} style={buttonStyle}> Deselect all pages </button> <span>Selected pages: {selectedPagesLabel}</span> </div> );};
export const AppPdfViewer: FC = () => { return ( <RPConfig licenseKey="YOUR_DOMAIN_TOKEN"> <RPProvider src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"> <ThumbnailPageSelector /> <RPLayout toolbar> <RPPages /> </RPLayout> </RPProvider> </RPConfig> );};