Basic Usage
Upload → preview (React)
import { useEffect, useRef, useState } from "react";
import {
createPdfPreview,
type PdfPreviewInstance,
} from "@marcusok/pdf-preview";
export function PdfPanel({ file }: { file: File }) {
const ref = useRef<HTMLDivElement>(null);
const previewRef = useRef<PdfPreviewInstance | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!ref.current) return;
const preview = createPdfPreview(ref.current, {
source: file,
scale: "fit-width",
onError: (e) => setError(`${e.code}: ${e.message}`),
});
previewRef.current = preview;
return () => preview.destroy(); // cancels in-flight work + worker
}, [file]);
return (
<div style={{ height: 600, overflow: "auto", background: "#f5f5f5" }}>
{error && <p>{error}</p>}
<div ref={ref} />
</div>
);
}The scroll container is your own element — the preview grows inside it and the viewport-driven lazy rendering follows along automatically. The observers root at the nearest ancestor that really clips the content: an ancestor with a constrained height (inline height, max-height) or one that is actually overflowing once the first page placeholder is inserted (which also picks up class-defined fixed heights and flex-resolved heights). An overflow: auto wrapper that merely grows with its content is skipped, and with no scrolling ancestor at all the observers fall back to the browser viewport — so the preload/release bands track the element that really scrolls.
Scale modes
createPdfPreview(el, { source, scale: "fit-width" }); // page width = container width (default)
createPdfPreview(el, { source, scale: "fit-page" }); // whole page fits the container
createPdfPreview(el, { source, scale: 1.25 }); // raw pdf.js viewport scalefit-* resolves once at creation. To change the scale later, destroy() and create a new instance — v1 intentionally ships no hot re-layout API.
Text layer on/off
createPdfPreview(el, { source, textLayer: false }); // pure canvas (no selection)The text layer is on by default: transparent spans exactly aligned with the canvas glyphs, so selection and copy work like real text.
Performance instrumentation
createPdfPreview(el, {
source,
onRendered: ({ pageCount, firstPageMs, durationMs }) => {
// pageCount: document total; firstPageMs: first painted page (canvas + text layer);
// durationMs: from the createPdfPreview call to that first paint
},
});For a 200-page synthetic text document, parse + first-page text extraction measures ~66 ms as a warm median locally (Node baseline; one-time pdf.js worker/module init is excluded by a warm-up run — see the package README) — the number end users care about is firstPageMs, which includes canvas paint.