React Examples
React Examples & Recipes
Section titled “React Examples & Recipes”Scope: React viewer toolkit (@scaryterry/pdfium/react).
Comprehensive examples for @scaryterry/pdfium/react. Each recipe includes a description, complete TSX code, and a list of key APIs used.
For the canonical flagship shells, start with PDFViewer and Editor. This page is for deeper recipes and lower-level composition patterns. For the public shell-free route, start with Headless.
All examples assume you have configured
PDFiumProviderwith validworkerUrlandwasmUrl(orwasmBinary).
In these snippets,workerUrlrefers to your app’s emitted worker entry URL.
Typical setup:const workerUrl = new URL('./pdfium.worker.ts', import.meta.url).toString().
wasmUrlis the URL forpdfium.wasm(for example, imported via?urlor served frompublic/).
See React setup for the exact worker/WASM wiring steps. Copy-paste snippets live in one place: Worker entry snippet and provider bootstrap snippet.
Table of Contents
Section titled “Table of Contents”Basic (Tier 1 — PDFViewer)
Section titled “Basic (Tier 1 — PDFViewer)”- Minimal PDF Viewer
- Styled Viewer
- Viewer with Thumbnails
- Viewer with Search
- Viewer with Download Button
- Custom Toolbar
Intermediate (Tier 2 — useViewerSetup)
Section titled “Intermediate (Tier 2 — useViewerSetup)”- Custom Viewer Layout
- Text Selection + Search
- Annotation Inspector
- Form-Enabled Viewer
- Imperative Page Navigation
- Custom Page Overlays
Advanced (Tier 3 — Low-level)
Section titled “Advanced (Tier 3 — Low-level)”Integration
Section titled “Integration”Basic (Tier 1 — PDFViewer)
Section titled “Basic (Tier 1 — PDFViewer)”1. Minimal PDF Viewer
Section titled “1. Minimal PDF Viewer”Zero-configuration viewer. Wrap PDFViewer inside PDFiumProvider and you get navigation, zoom, and scroll out of the box.
import { PDFiumProvider, PDFViewer } from '@scaryterry/pdfium/react';
function App() { return ( <PDFiumProvider wasmUrl={wasmUrl} workerUrl={workerUrl} initialDocument={{ data: pdfBytes, name: 'document.pdf' }} > <div style={{ height: '100vh' }}> <PDFViewer /> </div> </PDFiumProvider> );}Key APIs: PDFiumProvider, PDFViewer
2. Styled Viewer
Section titled “2. Styled Viewer”Apply Tailwind (or any CSS framework) classes to each section of the viewer using the classNames prop.
import { PDFiumProvider, PDFViewer } from '@scaryterry/pdfium/react';
function StyledViewer() { return ( <PDFiumProvider wasmUrl={wasmUrl} workerUrl={workerUrl}> <PDFViewer classNames={{ root: 'flex flex-col h-full bg-slate-50', toolbar: 'flex items-center gap-2 px-4 py-2 bg-white border-b shadow-sm', search: 'px-4 py-2 bg-yellow-50 border-b', content: 'flex flex-1 overflow-hidden', activityBar: 'bg-white border-r', panel: 'w-48 border-r bg-white overflow-y-auto', pages: 'flex-1 min-h-0', }} /> </PDFiumProvider> );}Key APIs: PDFViewer, PDFViewerClassNames
3. Viewer with Thumbnails
Section titled “3. Viewer with Thumbnails”Enable panel mode and open the thumbnail panel on mount. Users can click a thumbnail to jump to that page.
import { PDFiumProvider, PDFViewer } from '@scaryterry/pdfium/react';
function ViewerWithThumbnails() { return ( <PDFiumProvider wasmUrl={wasmUrl} workerUrl={workerUrl}> <div style={{ height: '100vh' }}> <PDFViewer panels={['thumbnails', 'bookmarks']} initialPanel="thumbnails" classNames={{ panel: 'thumbnail-sidebar', pages: 'main-pages', }} /> </div> </PDFiumProvider> );}Key APIs: PDFViewer (panels, initialPanel), ThumbnailStrip (rendered internally)
4. Viewer with Search
Section titled “4. Viewer with Search”Search is enabled by default (showSearch={true}). The built-in keyboard shortcut Ctrl+F (or Cmd+F on macOS) opens the search panel. Press Enter to jump to the next match and Shift+Enter for the previous match.
import { PDFiumProvider, PDFViewer } from '@scaryterry/pdfium/react';
function ViewerWithSearch() { return ( <PDFiumProvider wasmUrl={wasmUrl} workerUrl={workerUrl}> <div style={{ height: '100vh' }}> <PDFViewer showSearch keyboardShortcuts classNames={{ search: 'search-bar', }} /> </div> </PDFiumProvider> );}Key APIs: PDFViewer (showSearch, keyboardShortcuts), useKeyboardShortcuts, SearchPanel
5. Viewer with Download Button
Section titled “5. Viewer with Download Button”Add the shipped download control by passing it as children to DefaultToolbar.
import { DefaultToolbar, DefaultToolbarDownloadButton, PDFiumProvider, PDFViewer,} from '@scaryterry/pdfium/react';
function ViewerWithDownload() { return ( <PDFiumProvider wasmUrl={wasmUrl} workerUrl={workerUrl}> <div style={{ height: '100vh' }}> <PDFViewer> <DefaultToolbar> <DefaultToolbarDownloadButton fileName="my-document.pdf" /> </DefaultToolbar> </PDFViewer> </div> </PDFiumProvider> );}Use useDownload() directly only when you need a custom save trigger or non-toolbar download UX.
Key APIs: PDFViewer, DefaultToolbar (children), DefaultToolbarDownloadButton
6. Custom Toolbar
Section titled “6. Custom Toolbar”Build a completely custom toolbar using usePDFViewer() to access the grouped viewer state. This gives you full control over the UI whilst retaining all viewer behaviour.
import { PDFiumProvider, PDFViewer, usePDFViewer } from '@scaryterry/pdfium/react';
function MyToolbar() { const { viewer, toggleSearch, isSearchOpen, activePanel, togglePanel } = usePDFViewer(); const { navigation, zoom, fit, scroll } = viewer;
return ( <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 16px', borderBottom: '1px solid #e5e7eb' }}> {/* Navigation */} <button type="button" disabled={!navigation.canPrev} onClick={navigation.prev}> Previous </button> <span> Page {navigation.pageIndex + 1} of {navigation.pageCount} </span> <button type="button" disabled={!navigation.canNext} onClick={navigation.next}> Next </button>
<span aria-hidden="true">|</span>
{/* Zoom */} <button type="button" disabled={!zoom.canZoomOut} onClick={zoom.zoomOut}>-</button> <span>{Math.round(zoom.scale * 100)}%</span> <button type="button" disabled={!zoom.canZoomIn} onClick={zoom.zoomIn}>+</button>
<span aria-hidden="true">|</span>
{/* Fit controls */} <button type="button" onClick={fit.fitWidth} aria-pressed={fit.activeFitMode === 'page-width'} > Fit Width </button> <button type="button" onClick={fit.fitPage} aria-pressed={fit.activeFitMode === 'page-fit'} > Fit Page </button>
<span aria-hidden="true">|</span>
{/* Scroll mode */} <select value={scroll.scrollMode} onChange={(e) => scroll.setScrollMode(e.target.value as 'continuous' | 'single')} > <option value="continuous">Continuous</option> <option value="single">Single Page</option> </select>
<span aria-hidden="true">|</span>
{/* Toggles */} <button type="button" onClick={() => togglePanel('thumbnails')}> {activePanel === 'thumbnails' ? 'Hide Thumbnails' : 'Show Thumbnails'} </button> <button type="button" onClick={toggleSearch}> {isSearchOpen ? 'Close Search' : 'Search'} </button> </div> );}
function CustomToolbarViewer() { return ( <PDFiumProvider wasmUrl={wasmUrl} workerUrl={workerUrl}> <div style={{ height: '100vh' }}> <PDFViewer panels={['thumbnails', 'bookmarks']}> {({ isSearchOpen, activePanel }) => ( <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <MyToolbar /> {isSearchOpen && <PDFViewer.Search />} <div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}> {activePanel === 'thumbnails' && <PDFViewer.Thumbnails />} <PDFViewer.Pages style={{ flex: 1, minHeight: 0 }} /> </div> </div> )} </PDFViewer> </div> </PDFiumProvider> );}Key APIs: PDFViewer (render function children), usePDFViewer, PDFViewer.Pages, PDFViewer.Thumbnails, PDFViewer.Search, PDFViewerState
Intermediate (Tier 2 — useViewerSetup)
Section titled “Intermediate (Tier 2 — useViewerSetup)”7. Custom Viewer Layout
Section titled “7. Custom Viewer Layout”Drop below PDFViewer and compose your own layout with useViewerSetup, PDFDocumentView, and PDFToolbar. This gives you full control over the slot arrangement and toolbar composition.
import { PDFiumProvider, PDFDocumentView, PDFToolbar, ThumbnailStrip, useViewerSetup, usePDFiumDocument,} from '@scaryterry/pdfium/react';import { useRef } from 'react';import type { PDFDocumentViewHandle } from '@scaryterry/pdfium/react';
function CustomLayout() { const viewer = useViewerSetup({ initialScale: 1.2, initialScrollMode: 'continuous' }); const { document: doc } = usePDFiumDocument(); const docViewRef = useRef<PDFDocumentViewHandle>(null);
const handleThumbnailSelect = (pageIndex: number) => { viewer.navigation.setPageIndex(pageIndex); docViewRef.current?.scrollToPage(pageIndex); };
return ( <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}> {/* Toolbar with all slots */} <PDFToolbar viewer={viewer}> <PDFToolbar.Navigation> {({ getPrevProps, getInputProps, getNextProps, pageCount }) => ( <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}> <button {...getPrevProps()}>Prev</button> <input {...getInputProps()} style={{ width: 48, textAlign: 'center' }} /> <span>/ {pageCount}</span> <button {...getNextProps()}>Next</button> </div> )} </PDFToolbar.Navigation>
<PDFToolbar.Zoom> {({ getZoomOutProps, getZoomInProps, getResetProps, percentage }) => ( <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}> <button {...getZoomOutProps()}>-</button> <span>{percentage}%</span> <button {...getZoomInProps()}>+</button> <button {...getResetProps()}>Reset</button> </div> )} </PDFToolbar.Zoom>
<PDFToolbar.Fit> {({ getFitWidthProps, getFitPageProps }) => ( <div style={{ display: 'flex', gap: 4 }}> <button {...getFitWidthProps()}>Fit W</button> <button {...getFitPageProps()}>Fit P</button> </div> )} </PDFToolbar.Fit>
<PDFToolbar.ScrollMode> {({ getSelectProps, options }) => ( <select {...getSelectProps()}> {options.map((opt) => ( <option key={opt.value} value={opt.value}>{opt.label}</option> ))} </select> )} </PDFToolbar.ScrollMode> </PDFToolbar>
{/* Content area with sidebar and pages */} <div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}> <ThumbnailStrip document={viewer.document} pageCount={viewer.navigation.pageCount} currentPageIndex={viewer.navigation.pageIndex} onPageSelect={handleThumbnailSelect} style={{ width: 180, minHeight: 0 }} /> <PDFDocumentView ref={docViewRef} containerRef={viewer.container.ref} zoomAnchorRef={viewer.container.zoomAnchorRef} scrollMode={viewer.scroll.scrollMode} scale={viewer.zoom.scale} currentPageIndex={viewer.navigation.pageIndex} onCurrentPageChange={viewer.navigation.setPageIndex} showTextLayer showAnnotations showLinks style={{ flex: 1, minHeight: 0 }} /> </div> </div> );}
function App() { return ( <PDFiumProvider wasmUrl={wasmUrl} workerUrl={workerUrl}> <div style={{ height: '100vh' }}> <CustomLayout /> </div> </PDFiumProvider> );}Key APIs: useViewerSetup, PDFDocumentView, PDFDocumentViewHandle, PDFToolbar, PDFToolbar.Navigation, PDFToolbar.Zoom, PDFToolbar.Fit, PDFToolbar.ScrollMode, ThumbnailStrip
8. Text Selection + Search
Section titled “8. Text Selection + Search”Combine the text layer (for native text selection) with useDocumentSearch for find-in-document functionality. Search results are highlighted on each page.
import { PDFiumProvider, PDFDocumentView, SearchPanel, useViewerSetup, useDocumentSearch,} from '@scaryterry/pdfium/react';import { useMemo, useState } from 'react';
function TextSearchViewer() { const viewer = useViewerSetup(); const [query, setQuery] = useState('');
const search = useDocumentSearch(viewer.document, query);
const searchState = useMemo( () => search.totalMatches > 0 || search.isSearching ? { resultsByPage: search.resultsByPage, currentIndex: search.currentIndex, matchIndexMap: search.matchIndexMap, currentMatchPageIndex: search.currentMatchPageIndex, } : undefined, [search], );
return ( <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <SearchPanel query={query} onQueryChange={setQuery} totalMatches={search.totalMatches} currentIndex={search.currentIndex} isSearching={search.isSearching} onNext={search.next} onPrev={search.prev} style={{ padding: 8 }} /> <PDFDocumentView containerRef={viewer.container.ref} zoomAnchorRef={viewer.container.zoomAnchorRef} scrollMode={viewer.scroll.scrollMode} scale={viewer.zoom.scale} currentPageIndex={viewer.navigation.pageIndex} onCurrentPageChange={viewer.navigation.setPageIndex} showTextLayer search={searchState} style={{ flex: 1, minHeight: 0 }} /> </div> );}
function App() { return ( <PDFiumProvider wasmUrl={wasmUrl} workerUrl={workerUrl}> <div style={{ height: '100vh' }}> <TextSearchViewer /> </div> </PDFiumProvider> );}Key APIs: useViewerSetup, useDocumentSearch, PDFDocumentView (showTextLayer, search), SearchPanel
9. Annotation Inspector
Section titled “9. Annotation Inspector”Display annotation overlays on each page and list annotations in a sidebar using useAnnotations. Click an annotation in the list to highlight it on the page.
import { PDFiumProvider, PDFViewer, usePDFViewer, useAnnotations, AnnotationOverlay,} from '@scaryterry/pdfium/react';import type { PageOverlayInfo, SerialisedAnnotation } from '@scaryterry/pdfium/react';import { useState } from 'react';
function AnnotationSidebar() { const { viewer } = usePDFViewer(); const pageIndex = viewer.navigation.pageIndex; const { data: annotations } = useAnnotations(viewer.document, pageIndex);
if (!annotations || annotations.length === 0) { return <p style={{ padding: 12 }}>No annotations on this page.</p>; }
return ( <div style={{ padding: 12, overflowY: 'auto' }}> <h3>Annotations ({annotations.length})</h3> <ul style={{ listStyle: 'none', padding: 0 }}> {annotations.map((ann, i) => ( <li key={i} style={{ padding: '8px 12px', marginBottom: 4, backgroundColor: '#f0f9ff', borderRadius: 4, fontSize: 13, }} > <strong>{ann.subtype}</strong> {ann.contents && <p style={{ margin: '4px 0 0', color: '#6b7280' }}>{ann.contents}</p>} </li> ))} </ul> </div> );}
function AnnotationPageOverlay(info: PageOverlayInfo) { const { viewer } = usePDFViewer(); const { data: annotations } = useAnnotations(viewer.document, info.pageIndex);
if (!annotations || annotations.length === 0) return null;
return ( <AnnotationOverlay annotations={annotations} width={info.width} height={info.height} originalHeight={info.originalHeight} scale={info.scale} style={{ position: 'absolute', inset: 0, pointerEvents: 'none', zIndex: 30 }} /> );}
function App() { return ( <PDFiumProvider wasmUrl={wasmUrl} workerUrl={workerUrl}> <div style={{ height: '100vh' }}> <PDFViewer showAnnotations={false} renderPageOverlay={(info) => <AnnotationPageOverlay {...info} />} > {() => ( <div style={{ display: 'flex', height: '100%' }}> <div style={{ width: 280, borderRight: '1px solid #e5e7eb', overflowY: 'auto' }}> <AnnotationSidebar /> </div> <PDFViewer.Pages style={{ flex: 1, minHeight: 0 }} /> </div> )} </PDFViewer> </div> </PDFiumProvider> );}Key APIs: useAnnotations, AnnotationOverlay, PDFViewer (renderPageOverlay), PageOverlayInfo, SerialisedAnnotation
10. Form-Enabled Viewer
Section titled “10. Form-Enabled Viewer”Render form fields into the page bitmap and use the form action hooks to manage form state. After mutations (flatten, undo), the document revision is bumped automatically, which invalidates caches and re-renders pages.
import { PDFiumProvider, PDFViewer, usePDFViewer, useDocumentFormActions, usePageFormActions, FlattenFlags,} from '@scaryterry/pdfium/react';
function FormControls() { const { viewer } = usePDFViewer(); const { killFocus, setHighlight } = useDocumentFormActions(viewer.document); const { flatten, undo, canUndo } = usePageFormActions( viewer.document, viewer.navigation.pageIndex, );
return ( <div style={{ display: 'flex', gap: 8, padding: 8, borderTop: '1px solid #e5e7eb' }}> <button type="button" onClick={async () => { await killFocus(); }} > Kill Focus </button> <button type="button" onClick={async () => { const canDo = await canUndo(); if (canDo) await undo(); }} > Undo </button> <button type="button" onClick={async () => { await flatten(FlattenFlags.NormalDisplay); }} > Flatten Page </button> </div> );}
function FormViewer() { return ( <PDFiumProvider wasmUrl={wasmUrl} workerUrl={workerUrl}> <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}> <PDFViewer renderFormFields style={{ flex: 1 }}> {() => ( <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <PDFViewer.Pages renderFormFields style={{ flex: 1, minHeight: 0 }} /> <FormControls /> </div> )} </PDFViewer> </div> </PDFiumProvider> );}Key APIs: PDFViewer (renderFormFields), useDocumentFormActions, usePageFormActions, FlattenFlags
11. Imperative Page Navigation
Section titled “11. Imperative Page Navigation”Use the PDFDocumentViewHandle ref to programmatically scroll to any page with scrollToPage. This is useful for tables of contents, bookmark navigation, or external link handlers.
import { PDFiumProvider, PDFViewer, usePDFViewer, useBookmarks,} from '@scaryterry/pdfium/react';import type { Bookmark } from '@scaryterry/pdfium/react';
function BookmarkList() { const { viewer, documentViewRef } = usePDFViewer(); const { data: bookmarks } = useBookmarks(viewer.document);
const handleClick = (bookmark: Bookmark) => { if (bookmark.destination?.pageIndex !== undefined) { const pageIndex = bookmark.destination.pageIndex; viewer.navigation.setPageIndex(pageIndex); documentViewRef.current?.scrollToPage(pageIndex, 'smooth'); } };
if (!bookmarks || bookmarks.length === 0) { return <p style={{ padding: 12 }}>No bookmarks found.</p>; }
return ( <nav style={{ padding: 12 }}> <h3>Bookmarks</h3> <ul> {bookmarks.map((bm, i) => ( <li key={i}> <button type="button" onClick={() => handleClick(bm)} style={{ cursor: 'pointer' }}> {bm.title} </button> {bm.children && bm.children.length > 0 && ( <ul> {bm.children.map((child, j) => ( <li key={j}> <button type="button" onClick={() => handleClick(child)} style={{ cursor: 'pointer' }}> {child.title} </button> </li> ))} </ul> )} </li> ))} </ul> </nav> );}
function BookmarkViewer() { return ( <PDFiumProvider wasmUrl={wasmUrl} workerUrl={workerUrl}> <PDFViewer> {() => ( <div style={{ display: 'flex', height: '100vh' }}> <div style={{ width: 260, borderRight: '1px solid #e5e7eb', overflowY: 'auto' }}> <BookmarkList /> </div> <PDFViewer.Pages style={{ flex: 1, minHeight: 0 }} /> </div> )} </PDFViewer> </PDFiumProvider> );}Key APIs: PDFDocumentViewHandle (scrollToPage), usePDFViewer (documentViewRef), useBookmarks, Bookmark
12. Custom Page Overlays
Section titled “12. Custom Page Overlays”Use renderPageOverlay to draw custom content on top of each page. The PageOverlayInfo provides transformRect and transformPoint to convert PDF coordinates (bottom-left origin) to screen coordinates (top-left origin, CSS pixels).
import { PDFiumProvider, PDFViewer } from '@scaryterry/pdfium/react';import type { PageOverlayInfo } from '@scaryterry/pdfium/react';
/** Example: highlight a specific region in PDF coordinate space. */const highlights = [ { pageIndex: 0, rect: { left: 72, top: 720, right: 300, bottom: 700 } }, { pageIndex: 0, rect: { left: 72, top: 680, right: 540, bottom: 660 } }, { pageIndex: 1, rect: { left: 100, top: 500, right: 400, bottom: 480 } },];
function PageHighlights(info: PageOverlayInfo) { const pageHighlights = highlights.filter((h) => h.pageIndex === info.pageIndex); if (pageHighlights.length === 0) return null;
return ( <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}> {pageHighlights.map((h, i) => { const screen = info.transformRect(h.rect); return ( <div key={i} style={{ position: 'absolute', left: screen.x, top: screen.y, width: screen.width, height: screen.height, backgroundColor: 'rgba(255, 220, 0, 0.3)', border: '1px solid rgba(255, 180, 0, 0.6)', borderRadius: 2, }} /> ); })} </div> );}
function OverlayViewer() { return ( <PDFiumProvider wasmUrl={wasmUrl} workerUrl={workerUrl}> <div style={{ height: '100vh' }}> <PDFViewer renderPageOverlay={(info) => <PageHighlights {...info} />} /> </div> </PDFiumProvider> );}Key APIs: PDFViewer (renderPageOverlay), PageOverlayInfo (transformRect, transformPoint, pageIndex, width, height, originalWidth, originalHeight, scale)
Advanced (Tier 3 — Low-level)
Section titled “Advanced (Tier 3 — Low-level)”13. Standalone Page Renderer
Section titled “13. Standalone Page Renderer”Render a single PDF page without any viewer chrome. Uses useRenderPage directly with PDFCanvas. Useful for embedding a single page preview inside another UI.
import { PDFiumProvider, usePDFiumDocument, useRenderPage, PDFCanvas,} from '@scaryterry/pdfium/react';
function SinglePagePreview({ pageIndex, scale = 1 }: { pageIndex: number; scale?: number }) { const { document } = usePDFiumDocument(); const { renderKey, width, height, isLoading, error, retry } = useRenderPage(document, pageIndex, { scale, });
if (error) { return ( <div style={{ padding: 16 }}> <p>Render failed: {error.message}</p> <button type="button" onClick={retry}>Retry</button> </div> ); }
const displayWidth = width ? width / scale : 300; const displayHeight = height ? height / scale : 400;
return ( <div style={{ position: 'relative', display: 'inline-block' }}> {isLoading && ( <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: '#f3f4f6', }} > Loading... </div> )} <PDFCanvas width={width ?? 0} height={height ?? 0} renderKey={renderKey} style={{ width: displayWidth, height: displayHeight }} /> </div> );}
function App() { return ( <PDFiumProvider wasmUrl={wasmUrl} workerUrl={workerUrl}> <div style={{ display: 'flex', gap: 16, padding: 24 }}> <SinglePagePreview pageIndex={0} scale={2} /> <SinglePagePreview pageIndex={1} scale={2} /> </div> </PDFiumProvider> );}Key APIs: useRenderPage, PDFCanvas, usePDFiumDocument
14. PDF Thumbnail Generator
Section titled “14. PDF Thumbnail Generator”Generate small thumbnail images at a reduced scale. Renders each page at 0.3x and displays them in a grid. This is a lightweight alternative to ThumbnailStrip when you need custom thumbnail layouts.
import { PDFiumProvider, usePDFiumDocument, useRenderPage, PDFCanvas,} from '@scaryterry/pdfium/react';
function Thumbnail({ pageIndex }: { pageIndex: number }) { const { document } = usePDFiumDocument(); const thumbScale = 0.3; const { renderKey, width, height, originalWidth, originalHeight, isLoading } = useRenderPage( document, pageIndex, { scale: thumbScale }, );
const displayWidth = originalWidth ? originalWidth * thumbScale : 80; const displayHeight = originalHeight ? originalHeight * thumbScale : 100;
return ( <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, }} > <div style={{ width: displayWidth, height: displayHeight, backgroundColor: isLoading ? '#f3f4f6' : 'transparent', boxShadow: '0 1px 3px rgba(0,0,0,0.1)', overflow: 'hidden', }} > <PDFCanvas width={width ?? 0} height={height ?? 0} renderKey={renderKey} style={{ width: displayWidth, height: displayHeight }} /> </div> <span style={{ fontSize: 12, color: '#6b7280' }}>Page {pageIndex + 1}</span> </div> );}
function ThumbnailGrid() { const { document } = usePDFiumDocument(); const pageCount = document?.pageCount ?? 0;
return ( <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: 16, padding: 24, }} > {Array.from({ length: pageCount }, (_, i) => ( <Thumbnail key={i} pageIndex={i} /> ))} </div> );}
function App() { return ( <PDFiumProvider wasmUrl={wasmUrl} workerUrl={workerUrl}> <ThumbnailGrid /> </PDFiumProvider> );}Key APIs: useRenderPage (at scale 0.3), PDFCanvas, usePDFiumDocument
15. Document Creator
Section titled “15. Document Creator”Use usePDFiumInstance to access the worker-backed WorkerPDFium instance and create documents without leaving the worker runtime.
import { PDFiumProvider, PDFViewer, usePDFiumDocument, usePDFiumInstance,} from '@scaryterry/pdfium/react';import { useCallback, useState } from 'react';
function CreateDocumentButton() { const { instance } = usePDFiumInstance(); const { loadDocument } = usePDFiumDocument(); const [isCreating, setIsCreating] = useState(false);
const handleCreate = useCallback(async () => { if (!instance) return; setIsCreating(true); try { await using builder = await instance.createDocumentBuilder();
// Add an A4 page (595 x 842 points) const page = await builder.addPage({ width: 595, height: 842 });
// Add text using a standard font const font = await builder.loadStandardFont('Helvetica'); await page.addText('Hello from PDFium!', 72, 770, font, 24); await page.addText('This document was created in a worker.', 72, 740, font, 14);
// Save to bytes and load into the viewer const bytes = await builder.save(); await loadDocument(bytes, 'created-document.pdf'); } finally { setIsCreating(false); } }, [instance, loadDocument]);
return ( <button type="button" onClick={handleCreate} disabled={isCreating || !instance}> {isCreating ? 'Creating...' : 'Create Document'} </button> );}
function App() { return ( <PDFiumProvider wasmUrl={wasmUrl} workerUrl={workerUrl}> <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}> <div style={{ padding: 12, borderBottom: '1px solid #e5e7eb' }}> <CreateDocumentButton /> </div> <div style={{ flex: 1 }}> <PDFViewer /> </div> </div> </PDFiumProvider> );}Key APIs: usePDFiumInstance, WorkerPDFiumDocumentBuilder (addPage, loadStandardFont, save), usePDFiumDocument (loadDocument)
Integration
Section titled “Integration”16. Responsive Viewer
Section titled “16. Responsive Viewer”Use useFitZoom via the fit state from useViewerSetup to automatically fit the document to the container width on load. The fit mode is reapplied automatically when the container is resized or the page changes.
import { PDFiumProvider, PDFViewer, usePDFViewer,} from '@scaryterry/pdfium/react';import { useEffect, useRef } from 'react';
function FitOnLoad() { const { viewer } = usePDFViewer(); const hasFitted = useRef(false);
// Fit to width once the document is loaded and dimensions are known useEffect(() => { if (viewer.document && viewer.container.dimensions && !hasFitted.current) { viewer.fit.fitWidth(); hasFitted.current = true; } }, [viewer.document, viewer.container.dimensions, viewer.fit]);
// Reset the flag when the document changes useEffect(() => { hasFitted.current = false; }, [viewer.document]);
return null;}
function ResponsiveViewer() { return ( <PDFiumProvider wasmUrl={wasmUrl} workerUrl={workerUrl}> <div style={{ height: '100vh' }}> <PDFViewer> {() => ( <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <FitOnLoad /> <PDFViewer.Pages style={{ flex: 1, minHeight: 0 }} /> </div> )} </PDFViewer> </div> </PDFiumProvider> );}Once fitWidth() is called, useViewerSetup tracks the active fit mode internally. If the container resizes (e.g. the browser window is made narrower), the fit scale is recomputed and reapplied automatically. Manual zoom actions (scroll-wheel, buttons) clear the active fit mode.
Key APIs: usePDFViewer, FitState (fitWidth, fitPage, activeFitMode), ContainerState (dimensions)
17. Error Handling
Section titled “17. Error Handling”Combine PDFiumErrorBoundary for catching render-time errors, DragDropZone for file loading, and the password flow from usePDFiumDocument for a resilient viewer.
import { PDFiumProvider, PDFiumErrorBoundary, PDFViewer, DragDropZone, usePDFiumDocument,} from '@scaryterry/pdfium/react';import { useCallback } from 'react';
function PasswordDialog() { const { password } = usePDFiumDocument();
if (!password.required) return null;
return ( <div style={{ position: 'fixed', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(0, 0, 0, 0.5)', zIndex: 100, }} > <form onSubmit={async (e) => { e.preventDefault(); const form = e.currentTarget; const input = form.elements.namedItem('password') as HTMLInputElement; await password.submit(input.value); }} style={{ padding: 24, backgroundColor: 'white', borderRadius: 8, boxShadow: '0 4px 12px rgba(0,0,0,0.15)', minWidth: 300, }} > <h3 style={{ margin: '0 0 12px' }}>Password Required</h3> {password.error && ( <p style={{ color: '#dc2626', marginBottom: 8, fontSize: 14 }}>{password.error}</p> )} <input name="password" type="password" placeholder="Enter document password" autoFocus style={{ width: '100%', padding: 8, marginBottom: 12 }} /> <div style={{ display: 'flex', gap: 8 }}> <button type="submit" style={{ flex: 1 }}> Unlock </button> <button type="button" onClick={password.cancel}> Cancel </button> </div> </form> </div> );}
function ErrorFallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) { return ( <div role="alert" style={{ padding: 32, textAlign: 'center', backgroundColor: '#fef2f2', borderRadius: 8, }} > <h3>Something went wrong</h3> <p style={{ color: '#374151', fontSize: 14 }}>{error.message}</p> <button type="button" onClick={resetErrorBoundary} style={{ marginTop: 12, padding: '8px 16px' }} > Try Again </button> </div> );}
function ResilientViewer() { const { loadDocument, documentRevision, error } = usePDFiumDocument();
const handleFileSelect = useCallback( async (data: Uint8Array, name: string) => { await loadDocument(data, name); }, [loadDocument], );
return ( <> <PasswordDialog /> <PDFiumErrorBoundary resetKeys={[documentRevision]} fallbackRender={({ error: boundaryError, resetErrorBoundary }) => ( <ErrorFallback error={boundaryError} resetErrorBoundary={resetErrorBoundary} /> )} > <DragDropZone onFileSelect={handleFileSelect}> <PDFViewer /> </DragDropZone> </PDFiumErrorBoundary> {error && !error.message.includes('password') && ( <div role="alert" style={{ position: 'fixed', bottom: 16, right: 16, padding: 12, backgroundColor: '#fef2f2', border: '1px solid #fecaca', borderRadius: 6, fontSize: 14, }} > {error.message} </div> )} </> );}
function App() { return ( <PDFiumProvider wasmUrl={wasmUrl} workerUrl={workerUrl}> <div style={{ height: '100vh' }}> <ResilientViewer /> </div> </PDFiumProvider> );}Key APIs: PDFiumErrorBoundary (resetKeys, fallbackRender), DragDropZone (onFileSelect), usePDFiumDocument (password, loadDocument, error, documentRevision)
See also
Section titled “See also”- PDFViewer - Component tiers, slot APIs, and viewer state surface.
- useViewerSetup - Hook-level orchestration for custom viewer layouts.
- Styling Guide - CSS custom properties and class targeting strategy.