Skip to content

PDFViewer

Scope: React viewer toolkit (@scaryterry/pdfium/react).

<PDFViewer> is a compound component that assembles a full viewer from smaller building blocks. Start with the flagship shell (PDFViewer + DefaultToolbar + panels), then drop lower only when your product needs it.

TierWhat you useWhen to use
High-level<PDFViewer /> with propsZero-config viewer with sensible defaults. Customise via classNames, renderPageOverlay, or by passing children to replace the toolbar.
Mid-leveluseViewerSetup() + slot components (PDFViewer.Pages, PDFViewer.Thumbnails, PDFViewer.Search, PDFViewer.Bookmarks)You want to control layout and compose your own chrome, but still delegate page rendering, thumbnails, search, and bookmark navigation to pre-built components.
Low-levelIndividual hooks (usePageNavigation, useZoom, useRenderPage, useDocumentSearch, etc.)Full headless control. You build every piece of UI yourself and wire it to the hooks directly.

The snippets below assume you already have an ArrayBuffer named pdfBytes (from file input or fetch). They also assume workerUrl and wasmUrl are already resolved as shown in React setup. Use the canonical setup snippets from: Worker entry snippet and provider bootstrap snippet.

import { PDFiumProvider, PDFViewer } from '@scaryterry/pdfium/react';
function App() {
return (
<PDFiumProvider
wasmUrl={wasmUrl}
workerUrl={workerUrl}
initialDocument={{ data: pdfBytes, name: 'document.pdf' }}
>
<PDFViewer />
</PDFiumProvider>
);
}
<PDFiumProvider
wasmUrl={wasmUrl}
workerUrl={workerUrl}
initialDocument={{ data: pdfBytes, name: 'document.pdf' }}
>
<PDFViewer
panels={['thumbnails', 'bookmarks']}
classNames={{
root: 'flex flex-col h-full',
toolbar: 'bg-gray-100 border-b px-4 py-2',
search: 'border-b px-4 py-2',
content: 'flex flex-1 overflow-hidden',
activityBar: 'border-r',
panel: 'w-56 border-r overflow-y-auto',
pages: 'flex-1 min-h-0',
}}
/>
</PDFiumProvider>

PropTypeDefaultDescription
initialScalenumber1Initial zoom scale applied when the viewer mounts.
initialFitModeFitModeundefinedInitial fit mode applied when the viewer mounts. Use 'page-width' when the page should occupy the available width immediately. While active, it takes precedence over initialScale.
initialScrollMode'continuous' | 'single' | 'horizontal''continuous'Scroll mode on mount.
initialSpreadModeSpreadMode'none'Initial spread mode for page layout.
initialInteractionModeInteractionMode'pointer'Initial interaction mode (for tools such as marquee/select workflows).
showSearchbooleantrueWhether the search panel toggle is available. When false, the Ctrl/Cmd+F shortcut is also disabled.
panelsreadonly PanelEntry[]undefinedEnables panel mode and defines which panel tabs are available in the activity bar. Use built-in IDs ('thumbnails', 'bookmarks', etc.) or custom panel configs.
initialPanelPanelId | stringundefinedPanel to open on mount when panel mode is enabled.
initialPanelVisibility'always' | 'desktop-only''always'Whether initialPanel should open on every viewport or only when the shell has room for an inline sidebar. Use 'desktop-only' when mobile and tablet should start with the document as the primary surface.
showTextLayerbooleantrueRender the selectable text overlay on each page.
showAnnotationsbooleantrueRender annotation overlays on each page.
showLinksbooleantrueRender clickable link regions on each page.
renderFormFieldsbooleanfalseRender interactive form fields into the page bitmap.
gapnumber16Gap between pages in CSS pixels (continuous scroll mode).
bufferPagesnumber1Number of pages to render above and below the viewport for smoother scrolling.
keyboardShortcutsbooleantrueEnable built-in keyboard shortcuts for navigation, zoom, and search. See Keyboard Shortcuts.
renderPageOverlay(info: PageOverlayInfo) => ReactNodeundefinedCallback to render custom content on top of each page. Receives page geometry and a transformRect helper for coordinate conversion.
classNamestringundefinedCSS class applied to the root element. Merged with classNames.root when both are provided.
classNamesPDFViewerClassNamesundefinedPer-slot CSS class overrides. See classNames Reference.
styleCSSPropertiesundefinedInline styles for the root element (merged with the default flex-column layout).
childrenReactNode | ((state: PDFViewerState & PDFPanelState) => ReactNode)undefinedOverride the entire viewer body. Pass a ReactNode to replace the default layout, or a render function to receive viewer + panel state for full headless control.

The object passed to renderPageOverlay:

FieldTypeDescription
pageIndexnumberZero-based page index.
widthnumberScaled page width in CSS pixels.
heightnumberScaled page height in CSS pixels.
originalWidthnumberOriginal page width in PDF points.
originalHeightnumberOriginal page height in PDF points.
scalenumberCurrent zoom scale.
transformRect(rect) => { x, y, width, height }Converts a PDF rect (bottom-left origin) to a screen rect (top-left origin, CSS pixels).
transformPoint(point: { x, y }) => { x, y }Converts a PDF point (bottom-left origin) to a screen point (top-left origin, CSS pixels).

Each key targets a specific element in the default layout.

KeyHTML ElementDescription
rootOutermost <div>The root container. Uses display: flex; flex-direction: column; height: 100% by default. Merged with the className prop.
toolbar<div role="toolbar">The <DefaultToolbar> wrapper.
search<div>The <SearchPanel> wrapper, rendered only when search is open.
content<div>The middle row containing the sidebar and page area. Uses display: flex; flex: 1; overflow: hidden.
activityBar<div>The activity bar wrapper (panel mode only).
panel<div>The currently open panel container (panel mode only).
pages<div>The PDFViewer.Pages wrapper that hosts <PDFDocumentView>. Uses flex: 1; min-height: 0.

Slot components are attached to the PDFViewer function and read state from the nearest <PDFViewer> context. Use them when building a custom layout inside children.

Renders the main page area (virtualised PDFDocumentView).

<PDFViewer.Pages
gap={16}
bufferPages={2}
showTextLayer
showAnnotations
showLinks
renderFormFields={false}
/>
PropTypeDefaultDescription
classNamestringundefinedCSS class for the scroll container.
styleCSSPropertiesundefinedInline styles for the scroll container.
gapnumberundefinedGap between pages in CSS pixels.
bufferPagesnumberundefinedNumber of off-screen pages to pre-render.
showTextLayerbooleantrueRender selectable text overlay.
showAnnotationsbooleantrueRender annotation overlays.
showLinksbooleantrueRender clickable link regions.
renderFormFieldsbooleanfalseRender form fields into the page bitmap.
renderPageOverlay(info: PageOverlayInfo) => ReactNodeundefinedCustom per-page overlay callback.
loadingContentReactNodeundefinedContent to display while pages are loading.

Renders the thumbnail sidebar (ThumbnailStrip). Clicking a thumbnail navigates to that page and scrolls the main view.

<PDFViewer.Thumbnails thumbnailScale={0.2} className="w-48 border-r" />
PropTypeDefaultDescription
thumbnailScalenumberundefinedScale factor for thumbnail rendering.
classNamestringundefinedCSS class for the thumbnail strip.
styleCSSPropertiesundefinedInline styles (receives min-height: 0 by default).

Renders the search panel (SearchPanel). Wired to the viewer’s search state automatically.

<PDFViewer.Search className="border-b px-4 py-2" />
PropTypeDefaultDescription
classNamestringundefinedCSS class for the search panel.
styleCSSPropertiesundefinedInline styles for the search panel.

Renders the bookmark sidebar (BookmarkPanel). Displays the document’s table of contents as a collapsible tree. Clicking a bookmark navigates to that page and scrolls the main view. Includes a built-in filter input and full WAI-ARIA TreeView keyboard navigation.

<PDFViewer.Bookmarks defaultExpanded showFilter className="w-56 border-r" />
PropTypeDefaultDescription
defaultExpandedbooleanfalseStart with all parent nodes expanded.
showFilterbooleantrueShow a filter input at the top of the tree.
classNamestringundefinedCSS class for the bookmark panel.
styleCSSPropertiesundefinedInline styles (receives min-height: 0 by default).

Returns the full PDFViewerState from the nearest <PDFViewer> context. Throws if called outside a <PDFViewer>.

import { usePDFViewer } from '@scaryterry/pdfium/react';
function CustomToolbar() {
const { viewer, isSearchOpen, toggleSearch } = usePDFViewer();
// ...
}
FieldTypeDescription
viewerUseViewerSetupResultGrouped viewer state. See sub-tables below.
searchUseDocumentSearchResultCross-document search results: matches, resultsByPage, matchIndexMap, currentIndex, totalMatches, isSearching, currentMatchPageIndex, next(), prev(), goToMatch(index).
searchQuerystringCurrent search query string.
setSearchQuery(query: string) => voidUpdate the search query (triggers a debounced search).
isSearchOpenbooleanWhether the search panel is currently visible.
toggleSearch() => voidToggle search panel visibility. Clears the query when closing.
activePanelstring | nullCurrently open panel ID, or null when no panel is open.
togglePanel(id: string) => voidToggle a panel by ID (open if closed, close if already active).
setPanelOverlay(renderer: ((info: PageOverlayInfo) => ReactNode) | null) => voidRegister/clear a panel-specific page overlay renderer.
hasPanelBarbooleanWhether panel mode is enabled (panels prop provided and non-empty).
documentViewRefRefObject<PDFDocumentViewHandle | null>Imperative handle for the page scroll container. Exposes scrollToPage(pageIndex, behaviour?).

The viewer field is a composite of five state groups:

FieldTypeDescription
pageIndexnumberCurrent zero-based page index.
setPageIndex(index: number) => voidJump to a specific page.
next() => voidNavigate to the next page.
prev() => voidNavigate to the previous page.
canNextbooleantrue if there is a next page.
canPrevbooleantrue if there is a previous page.
pageCountnumberTotal number of pages in the document.
FieldTypeDescription
scalenumberCurrent zoom scale (1 = 100%).
setScale(scale: number) => voidSet an exact scale value. Clears any active fit mode.
zoomIn() => voidIncrement zoom by one step. Clears any active fit mode.
zoomOut() => voidDecrement zoom by one step. Clears any active fit mode.
reset() => voidReset zoom to the initial scale. Clears any active fit mode.
canZoomInbooleantrue if zoom has not reached the maximum.
canZoomOutbooleantrue if zoom has not reached the minimum.
FieldTypeDescription
fitWidth() => voidZoom to fit the page width within the container. Sets activeFitMode to 'page-width'.
fitPage() => voidZoom to fit the entire page within the container. Sets activeFitMode to 'page-fit'.
fitScale(mode: FitMode) => numberCompute the scale for a given fit mode without applying it.
activeFitModeFitMode | nullCurrently active fit mode ('page-width', 'page-height', 'page-fit'), or null if the user has manually zoomed. Auto-reapplied on container resize.
FieldTypeDescription
scrollMode'continuous' | 'single'Current scroll mode.
setScrollMode(mode: 'continuous' | 'single') => voidSwitch between scroll modes.
FieldTypeDescription
refRefObject<HTMLDivElement | null>Ref to the scroll container element. Attach to the element that should receive scroll events.
dimensionsPageDimension[] | undefinedArray of { width, height } for every page in PDF points. undefined while loading.
zoomAnchorRefRefObject<ZoomAnchor | null>Internal ref used to coordinate cursor-anchored zoom between useWheelZoom and useVisiblePages.

The simplest possible viewer. The default layout includes DefaultToolbar, search, and a full-page scroll area.

import { PDFiumProvider, PDFViewer } from '@scaryterry/pdfium/react';
function App() {
return (
<PDFiumProvider
wasmUrl={wasmUrl}
workerUrl={workerUrl}
initialDocument={{ data: pdfBytes, name: 'report.pdf' }}
>
<PDFViewer />
</PDFiumProvider>
);
}

Apply Tailwind classes to each slot without changing structure or behaviour.

import { PDFiumProvider, PDFViewer } from '@scaryterry/pdfium/react';
function App() {
return (
<PDFiumProvider
wasmUrl={wasmUrl}
workerUrl={workerUrl}
initialDocument={{ data: pdfBytes, name: 'report.pdf' }}
>
<PDFViewer
panels={['thumbnails', 'bookmarks']}
initialPanel="thumbnails"
classNames={{
root: 'flex flex-col h-screen bg-gray-50',
toolbar: 'flex items-center gap-2 bg-white border-b border-gray-200 px-4 py-2 shadow-sm',
search: 'bg-yellow-50 border-b border-yellow-200 px-4 py-2',
content: 'flex flex-1 overflow-hidden',
activityBar: 'bg-white border-r border-gray-200',
panel: 'bg-gray-100 border-r border-gray-200 overflow-y-auto',
pages: 'flex-1 min-h-0 bg-gray-200',
}}
/>
</PDFiumProvider>
);
}

3. Replace toolbar with custom ReactNode children

Section titled “3. Replace toolbar with custom ReactNode children”

When children is provided, the entire default layout is replaced. Use slot components to compose your own arrangement.

import { PDFiumProvider, PDFViewer, usePDFViewer } from '@scaryterry/pdfium/react';
function CustomToolbar() {
const { viewer, toggleSearch } = usePDFViewer();
return (
<header className="flex items-center gap-4 p-3 bg-blue-600 text-white">
<button onClick={viewer.navigation.prev} disabled={!viewer.navigation.canPrev}>
Previous
</button>
<span>
Page {viewer.navigation.pageIndex + 1} of {viewer.navigation.pageCount}
</span>
<button onClick={viewer.navigation.next} disabled={!viewer.navigation.canNext}>
Next
</button>
<button onClick={viewer.zoom.zoomOut}>Zoom Out</button>
<span>{Math.round(viewer.zoom.scale * 100)}%</span>
<button onClick={viewer.zoom.zoomIn}>Zoom In</button>
<button onClick={toggleSearch}>Search</button>
</header>
);
}
function App() {
return (
<PDFiumProvider
wasmUrl={wasmUrl}
workerUrl={workerUrl}
initialDocument={{ data: pdfBytes, name: 'report.pdf' }}
>
<PDFViewer>
<div className="flex flex-col h-screen">
<CustomToolbar />
<PDFViewer.Pages className="flex-1 min-h-0" />
</div>
</PDFViewer>
</PDFiumProvider>
);
}

<DefaultToolbar> accepts children that are appended after the built-in controls. This is the easiest way to add shipped extras like document download or your own custom actions without rebuilding the entire toolbar.

import {
DefaultToolbar,
DefaultToolbarDownloadButton,
PDFiumProvider,
PDFViewer,
} from '@scaryterry/pdfium/react';
function App() {
return (
<PDFiumProvider
wasmUrl={wasmUrl}
workerUrl={workerUrl}
initialDocument={{ data: pdfBytes, name: 'report.pdf' }}
>
<PDFViewer>
<div className="flex flex-col h-screen">
<DefaultToolbar>
<DefaultToolbarDownloadButton />
</DefaultToolbar>
<PDFViewer.Pages className="flex-1 min-h-0" />
</div>
</PDFViewer>
</PDFiumProvider>
);
}

Pass a function as children to receive PDFViewerState and build the entire UI yourself. No default layout is rendered.

import { PDFiumProvider, PDFViewer } from '@scaryterry/pdfium/react';
function App() {
return (
<PDFiumProvider
wasmUrl={wasmUrl}
workerUrl={workerUrl}
initialDocument={{ data: pdfBytes, name: 'report.pdf' }}
>
<PDFViewer panels={['thumbnails', 'bookmarks']}>
{(state) => (
<div className="flex flex-col h-screen">
<div className="p-4 bg-gray-800 text-white flex items-center gap-4">
<button onClick={state.viewer.navigation.prev} disabled={!state.viewer.navigation.canPrev}>
Back
</button>
<span>
{state.viewer.navigation.pageIndex + 1} / {state.viewer.navigation.pageCount}
</span>
<button onClick={state.viewer.navigation.next} disabled={!state.viewer.navigation.canNext}>
Forward
</button>
<span>{Math.round(state.viewer.zoom.scale * 100)}%</span>
<button onClick={state.toggleSearch}>
{state.isSearchOpen ? 'Close' : 'Find'}
</button>
<button onClick={() => state.togglePanel('thumbnails')}>
{state.activePanel === 'thumbnails' ? 'Hide Thumbnails' : 'Show Thumbnails'}
</button>
<button onClick={() => state.togglePanel('bookmarks')}>
{state.activePanel === 'bookmarks' ? 'Hide Bookmarks' : 'Show Bookmarks'}
</button>
</div>
{state.isSearchOpen && <PDFViewer.Search />}
<div className="flex flex-1 overflow-hidden">
{state.activePanel === 'thumbnails' && <PDFViewer.Thumbnails className="w-48 border-r" />}
{state.activePanel === 'bookmarks' && <PDFViewer.Bookmarks className="w-56 border-r" />}
<PDFViewer.Pages className="flex-1 min-h-0" showTextLayer showAnnotations />
</div>
</div>
)}
</PDFViewer>
</PDFiumProvider>
);
}

<DefaultToolbar> is the shipped default toolbar rendered inside <PDFViewer>. It provides accessible native HTML controls for navigation, zoom, fit mode, scroll mode, and search toggle, and is designed to be themed or extended before you replace it outright.

PropTypeDefaultDescription
viewerUseViewerSetupResultundefinedOverride the viewer state used for navigation, zoom, fit, and scroll controls. By default, reads from the nearest <PDFViewer> context. Can also be used outside <PDFViewer> by passing an explicit viewer — the search toggle will be omitted since search state lives in the <PDFViewer> context.
classNamestringundefinedCSS class for the toolbar wrapper.
styleCSSPropertiesundefinedInline styles for the toolbar wrapper.
childrenReactNodeundefinedAdditional content appended after the default toolbar controls.

The toolbar renders five control groups separated by dividers:

  1. Navigation — Previous/Next buttons with a page number input and total count.
  2. Zoom — Zoom out/in buttons with a percentage display.
  3. Fit — Fit Width and Fit Page buttons.
  4. Scroll Mode — A <select> to switch between continuous and single-page modes.
  5. Search — A toggle button that opens/closes the search panel.

All shortcuts are enabled by default (keyboardShortcuts={true}). They call preventDefault() to avoid browser default behaviour (e.g. Ctrl+F opening the browser’s find bar, Ctrl+= zooming the browser page).

Arrow key shortcuts are suppressed when focus is inside a text input, textarea, or contentEditable element.

<DefaultToolbar> also uses roving focus inside the toolbar itself: ArrowLeft, ArrowRight, Home, and End move between toolbar controls without leaving the toolbar group, and supported buttons expose aria-keyshortcuts metadata for assistive technology.

ShortcutAction
HomeFirst page
ArrowRight / ArrowDown / PageDownNext page
ArrowLeft / ArrowUp / PageUpPrevious page
EndLast page
Ctrl/Cmd + =Zoom in
Ctrl/Cmd + -Zoom out
Ctrl/Cmd + 0Reset zoom
Ctrl/Cmd + FToggle search panel
Ctrl/Cmd + ]Rotate clockwise
Ctrl/Cmd + [Rotate counter-clockwise
Ctrl/Cmd + PPrint
F11Toggle fullscreen
VPointer / select-text mode
HHand tool
ZMarquee zoom
EnterNext search match (when search is open)
Shift + EnterPrevious search match (when search is open)

The default layout applies the following ARIA roles:

ElementARIA RoleNotes
Toolbar (<DefaultToolbar>)toolbarGroups navigation, zoom, and search controls.
Document view (<PDFDocumentView>)documentThe scrollable page area, indicating primary content.
Thumbnail strip (<ThumbnailStrip>)listboxEach thumbnail is a selectable option within the list.
Bookmark panel (<BookmarkPanel>)treeWAI-ARIA TreeView with treeitem nodes, aria-expanded, aria-level, aria-setsize, aria-posinset. Full keyboard navigation (Arrow keys, Home, End, Enter, Space, *).

The <DefaultToolbar> renders native <button>, <input>, and <select> elements, which are keyboard-focusable and screen-reader-accessible by default. Custom toolbars should preserve equivalent semantics.

  • Toolbar - DefaultToolbar and headless PDFToolbar composition model.
  • useViewerSetup - Orchestration hook that powers viewer navigation and zoom state.
  • Styling Guide - classNames targeting and CSS variable theming.
  • Examples - End-to-end integration patterns.