Getting Started
The headless libraries give you the engine and the plugin system — you own every pixel of the UI. An app depends on two packages: the adapter for your framework, and an engine.
Installation#
npm install @embedpdf/react @embedpdf/engineThe React adapter exposes every feature as one import line per vertical — registration, components, and hooks travel together, and deleting the line removes the feature from your bundle.
Your first viewer#
localEngine() creates the engine — synchronously, allocating nothing until
first use, so a module-scope const engine = localEngine() is safe (even under
SSR). Hand it to <Viewer>: the viewer warms it up on mount and PDFium boots
in a worker in the background, so the stage and render layer draw the pages the
moment a document is ready — no worker wiring, no lifecycle to manage. This is
the whole app:
Loading live preview…
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { localEngine } from '@embedpdf/engine';
// `localEngine()` IS the engine — created synchronously, costing nothing until
// first use (no worker, no WASM). Safe at module scope, even under SSR. The
// viewer warms it up on mount, PDFium boots in the background in a worker, and
// only opening a document awaits it — the UI renders at t≈0.
const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin()];
// The local engine opens bytes: fetch lazily, under the loading tab.
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<div style={{ height: 500 }}>
{/* Document UI is defined over a document — gate it on having one. */}
<DocumentGate fallback={<p>Loading…</p>}>
<Stage style={{ height: '100%' }}>{() => <RenderLayer />}</Stage>
</DocumentGate>
</div>
</Viewer>
);
}Prefer the viewer to own the engine’s lifetime — created on mount, destroyed on
unmount? Pass a thunk instead: engine={() => localEngine()}. See the
Engine getting started for the ownership model,
fallback fonts, cloud, and SSR.
Your feedback goes directly to the documentation team.