---
title: "Quick start — Vue"
description: "Build a PDF viewer step by step, with text selection, zoom buttons, a highlighter and a save button."
framework: "Vue"
source: "https://www.embedpdf.com/docs/headless/vue/quick-start"
---

# Quick start

This guide builds a small PDF viewer: the pages, text people can select, zoom buttons, a
highlighter and a save button. Each step adds one plugin or one control, so you can stop at any
step and have a working viewer.

## 1. Install

You need two packages: the one for your framework, and an engine.

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

Every feature is a separate import from the framework package, such as `@embedpdf/vue/stage`,
so what you don't use never reaches your bundle.

## 2. Show a PDF

Create the engine once, then give it to `<Viewer>` with the plugins you want and the document to
open. `<Stage>` shows the pages, and `<RenderLayer>` draws each one:

**`App.vue`**

```vue
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { localEngine } from '@embedpdf/engine';

// The engine is created synchronously and costs nothing until first use, so
// a module-level `const engine = …` is safe, even with server-side rendering.
// Only opening a document does real work: the UI renders at once.
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()) };
};
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <div style="height: 500px">
      <!-- Document UI is defined over a document: gate it on having one. -->
      <DocumentGate>
        <template #fallback><p>Loading…</p></template>
        <Stage style="height: 100%">
          <template #page><RenderLayer /></template>
        </Stage>
      </DocumentGate>
    </div>
  </Viewer>
</template>
```

`localEngine()` costs nothing until it's used, so creating it at the top of a module is safe, even
with server-side rendering. The viewer starts it when it mounts: PDFium boots in a Web Worker in
the background, and the first page shows as soon as the document is open. The engine's
WebAssembly is part of your build and loads from your own site; see [Setup](setup).

`<DocumentGate>` shows its `fallback` until the document is open, so the Stage never renders
without one.

## 3. Let people select text

Add the interaction plugin, which handles the pointer for every feature, and the selection plugin.
Then put `<SelectionLayer>` above the page:

**`select-text.vue`**

```vue
<script setup lang="ts">
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/vue/runtime';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { SelectionLayer, selectionPlugin } from '@embedpdf/vue/selection';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { localEngine } from '@embedpdf/engine';

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

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()) };
};
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <Stage style="height: 500px">
        <template #page>
          <RenderLayer />
          <SelectionLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

Drag over text to select it, double-click a word, or triple-click a line. On a phone, long-press a
word. See [Selecting & copying](text/selection) for copying and a selection menu.

## 4. Add zoom buttons

Every plugin has a composable for its API and one for its state. `useStage()` does things,
and `useStageState()` gives you what to show:

**`zoom-controls.vue`**

```vue
<script setup lang="ts">
import { useStage, useStageState } from '@embedpdf/vue/stage';

const stage = useStage();
const { zoomLevel } = useStageState();
</script>

<template>
  <div>
    <button @click="stage.zoomOut()">−</button>
    <span>{{ Math.round(zoomLevel * 100) }}%</span>
    <button @click="stage.zoomIn()">+</button>
  </div>
</template>
```

Put `<ZoomControls />` anywhere inside `<DocumentGate>`. It updates when the zoom
changes, whether a button, a pinch or code changed it.

## 5. Add a highlighter

The annotation plugin shows the annotations a PDF already has, and lets people add their own. Add
it with its layer, and a button that turns on the highlight tool:

**`App.vue`**

```vue
<script setup lang="ts">
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/vue/runtime';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/vue/annotation';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { SelectionLayer, selectionPlugin } from '@embedpdf/vue/selection';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { localEngine } from '@embedpdf/engine';
import HighlightButton from './HighlightButton.vue';

const engine = localEngine();
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  selectionPlugin(),
  annotationPlugin(),
];

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()) };
};
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <HighlightButton />
      <Stage style="height: 500px">
        <template #page>
          <RenderLayer />
          <SelectionLayer />
          <AnnotationLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`HighlightButton.vue`**

```vue
<script setup lang="ts">
import { computed } from 'vue';
import { useInteraction, useInteractionState } from '@embedpdf/vue/interaction';

const interaction = useInteraction();
const { activeToolId } = useInteractionState();
const on = computed(() => activeToolId.value === 'highlight');
</script>

<template>
  <button :aria-pressed="on" @click="interaction.activateTool(on ? 'pointer' : 'highlight')">
    Highlight
  </button>
</template>
```

With the tool on, select text and it's highlighted. Click a highlight with the `pointer` tool to
select it. The page picture leaves the annotations out by itself, because the annotation layer
draws them now. See [Annotations](annotations) for every
tool.

## 6. Save

`documents.download()` gives you the PDF with every change, as bytes. `saveFile` hands them to the
browser as a download:

**`save-button.vue`**

```vue
<script setup lang="ts">
import { saveFile, useDocuments } from '@embedpdf/vue/runtime';

const documents = useDocuments();

async function save() {
  saveFile(await documents.download(), 'reviewed.pdf');
}
</script>

<template>
  <button @click="save">Save</button>
</template>
```

Put `<SaveButton />` anywhere inside `<Viewer>`.


## Next steps

- [Setup](setup) — Server-side rendering, bundlers, fonts, and hosting the engine yourself.
- [How it works](concepts/how-it-works) — Plugins, how your UI talks to them, layers, and which document a component talks to.
- [Scroll, zoom & layout](viewing/stage) — Layouts, zoom modes, page navigation and touch.
