---
title: "Setup — Vue"
description: "Install the headless packages for your framework, create the engine once, and use them with server-side rendering."
framework: "Vue"
source: "https://www.embedpdf.com/docs/headless/vue/setup"
---

# 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

```sh
pnpm add @embedpdf/vue@next @embedpdf/engine@next
```

The 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:

```ts
// 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.vue`**

```vue
<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:

**`plugins.ts`**

```typescript
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:

**`ssr.vue`**

```vue
<!-- 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](https://www.embedpdf.com/docs/viewer/vue/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:

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

[Fonts](https://www.embedpdf.com/docs/engine/documents/fonts) covers registering fonts and using them by name. Every other
engine option is in the engine's [Setup](https://www.embedpdf.com/docs/engine/setup#options).

> **The same viewer, on your server**
>
> Move a headless viewer to CloudPDF by changing one line: `localEngine()` becomes
> `cloudEngine()`. Your plugins, components and composables stay as they are.
> Files stay on your server, the first page shows without downloading the PDF, and fonts, limits and
> storage are the server's to configure.
>
> [Set up CloudPDF](https://www.cloudpdf.com/docs/headless/vue/setup)

## Good to know

### One viewer, many documents

A viewer can hold several open documents at once, like tabs. See
[Several documents](documents/tabs).

### 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](https://www.embedpdf.com/docs/viewer/vue) uses these same packages. If you start there, everything on
> these pages still applies when you move to headless.
