EmbedPDF

Setup

A headless viewer needs two packages: the one for your framework, and an engine. This page installs them, creates the engine for your whole app, and covers server-side rendering, bundlers and fonts.

Install#

npm install @embedpdf/react@next @embedpdf/engine@next

The framework package holds every plugin, one import path each (@embedpdf/react/search, @embedpdf/react/annotation). A plugin you don’t import isn’t in your bundle.

It works with React 18 and later.

Create the engine once#

Create one engine when your app starts, and share it with every viewer:

// pdf.ts
import { localEngine } from '@embedpdf/engine';
 
export const engine = localEngine();

Creating the engine is instant and allocates nothing. The first viewer that uses it starts it, and PDFium boots in a Web Worker in the background.

<Viewer> can also own the engine. Pass a function, and the viewer creates the engine when it mounts and destroys it when it unmounts:

owned-engine.tsx
import { Viewer } from '@embedpdf/react/runtime';
import { localEngine } from '@embedpdf/engine';
import { plugins } from './pdf';

export function DocumentViewer() {
  return <Viewer engine={() => localEngine()} plugins={plugins} />;
}

An engine you pass in yourself is never destroyed by the viewer, so one engine can serve every viewer in your app.

Register the plugins#

Plugins are a list you create once, next to the engine:

plugins.ts
import { stagePlugin } from '@embedpdf/react/stage';
import { renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { selectionPlugin } from '@embedpdf/react/selection';

export const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), selectionPlugin()];

The viewer reads engine and plugins once, when it mounts. Creating the list inside a component would make a new list on every render; the viewer ignores it and warns you while you develop.

Order doesn’t matter, and a plugin that needs another says so when the app starts: the annotation plugin, for example, needs the interaction plugin.

Server-side rendering#

The headless packages render nothing on the server that needs the engine: the engine only starts in the browser. So a module-level engine is safe everywhere.

In the Next.js App Router, mark the component that renders <Viewer> as a client component:

ssr.tsx
'use client';

import { Viewer } from '@embedpdf/react/runtime';
import { engine, plugins } from './pdf';

export function DocumentViewer() {
  return <Viewer engine={engine} plugins={plugins}>{/* … */}</Viewer>;
}

Bundlers and self-hosting#

The engine’s worker and WebAssembly ship inside the package, and your build picks them up.

Vite, webpack 5 and Rspack need no configuration, and neither does Next.js, with webpack or Turbopack.

There’s nothing to copy, and nothing is fetched from a CDN. The engine loads from your own site, so it works offline and behind a firewall.

If your setup can’t bundle a worker, for example with a strict Content Security Policy, host the files yourself: Self-hosting lists every option, with notes for Parcel and plain esbuild.

Fonts#

Text the engine draws, such as a text box or a filled form field, needs a font with its characters. For languages the built-in fonts don’t cover, give the engine a fallback font:

export const engine = localEngine({
  fallbackFonts: [{ key: 'noto-cjk', familyName: 'Noto Sans CJK', url: '/fonts/NotoSansCJK.ttf' }],
});

Fonts covers registering fonts and using them by name. Every other engine option is in the engine’s Setup.

Good to know#

One viewer, many documents#

A viewer can hold several open documents at once, like tabs. See Several documents.

Changing engine or plugins#

Both are read once. To use another engine or another set of plugins, mount a new <Viewer>, for example by giving it a new key.

The ready-made Viewer uses these same packages. If you start there, everything on these pages still applies when you move to headless.

Was this page helpful?

Your feedback goes directly to the documentation team.