The PDF Viewer renders a document with pdf.js
directly — no react-pdf wrapper. It does continuous-scroll rendering, zoom,
rotate, fit-to-width, and download, and exposes a per-page overlay slot so you
can draw bounding-box citations (edit fields, extraction sources) on top of the
rendered page.
It is built without useEffect: document and page loading use React's use()
with Suspense, and each page renders into its canvas from a ref callback (with a
cleanup that cancels the in-flight render).
Installation
pnpm dlx shadcn@latest add @retab/pdf-viewer
This pulls in pdfjs-dist. The worker is resolved from the installed package by
your bundler (Vite, webpack, or Turbopack) — there is no runtime CDN call.
Usage
import { PdfViewer } from "@/components/ui/pdf-viewer"
export function Example() {
return <PdfViewer src="/document.pdf" className="h-[600px]" />
}Bounding-box overlays
renderPageOverlay runs per page and receives the rendered page size, so
normalized boxes map with simple percentages:
<PdfViewer
src={url}
renderPageOverlay={({ pageNumber }) =>
fieldsOnPage(pageNumber).map((f) => (
<div
key={f.key}
className="absolute outline outline-2 outline-indigo-500"
style={{
left: `${f.bbox.left * 100}%`,
top: `${f.bbox.top * 100}%`,
width: `${f.bbox.width * 100}%`,
height: `${f.bbox.height * 100}%`,
}}
/>
))
}
/>Props
| Prop | Type | Description |
|---|---|---|
src | string | URL of the PDF (same-origin or CORS-enabled). |
scale | number | Optional fixed scale; omit to fit page width to the container. |
toolbar | boolean | Show the zoom/rotate/download toolbar. Defaults to true. |
downloadFileName | string | Filename for the download button. |
renderPageOverlay | (props) => ReactNode | Per-page overlay; receives { pageNumber, width, height, scale, rotation }. |
className | string | Optional class on the viewer shell. |