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/vue@next @embedpdf/engine@nextThe framework package holds every plugin, one import path each (@embedpdf/vue/search,
@embedpdf/vue/annotation). A plugin you don’t import isn’t in your bundle.
It’s written for Vue 3: components for your templates and composables for <script setup>.
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:
<script setup lang="ts">
import { Viewer } from '@embedpdf/vue/runtime';
import { localEngine } from '@embedpdf/engine';
import { plugins } from './pdf';
</script>
<template>
<Viewer :engine="() => localEngine()" :plugins="plugins" />
</template>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:
import { stagePlugin } from '@embedpdf/vue/stage';
import { renderPlugin } from '@embedpdf/vue/render';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { selectionPlugin } from '@embedpdf/vue/selection';
export const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), selectionPlugin()];The viewer reads engine and plugins once, when it mounts, and ignores later changes to them.
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 Nuxt, render the viewer on the client only, inside <ClientOnly>. The server renders its
#fallback slot instead:
<!-- pages/contract.vue -->
<template>
<ClientOnly>
<DocumentViewer />
<template #fallback>
<div style="height: 600px">Loading the viewer…</div>
</template>
</ClientOnly>
</template>A component named DocumentViewer.client.vue renders on the client only wherever you use it, with
no <ClientOnly> around it.
Bundlers and self-hosting#
The engine’s worker and WebAssembly ship inside the package, and your build picks them up.
Vite, and so Nuxt, needs no configuration, and neither do webpack 5 and Rspack.
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.
Your feedback goes directly to the documentation team.