---
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.cloudpdf.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 @cloudpdf/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 { cloudEngine } from '@cloudpdf/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 = cloudEngine();
const plugins = [stagePlugin(), renderPlugin()];

const welcome: OpenInput = { kind: 'grant', grant: 'shr_gvpJcG1ULUMon19C0D1wODSY' };
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: welcome }]">
    <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>
```

`cloudEngine()` holds an HTTP client and nothing else, so creating it at the top of a
module is safe, even with server-side rendering. Every page, text and annotation call goes to your
CloudPDF server; 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 { cloudEngine } from '@cloudpdf/engine';

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

const welcome: OpenInput = { kind: 'grant', grant: 'shr_gvpJcG1ULUMon19C0D1wODSY' };
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: welcome }]">
    <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 { cloudEngine } from '@cloudpdf/engine';
import HighlightButton from './HighlightButton.vue';

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

const welcome: OpenInput = { kind: 'grant', grant: 'shr_gvpJcG1ULUMon19C0D1wODSY' };
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: welcome }]">
    <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>`.


You don't need to save to keep the highlights: the server stored each one as it was made.
`download()` is for when you want the file itself.

## 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.
