---
title: "Getting Started — Vue"
description: "Build your own PDF viewer UI with the EmbedPDF headless libraries."
framework: "Vue"
source: "https://www.embedpdf.com/docs/headless/vue/getting-started"
---

# Getting Started

The headless libraries give you the engine and the plugin system — you own
every pixel of the UI. An app depends on two packages: the adapter for your
framework, and an engine.

## Installation

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

The Vue adapter mirrors the React surface subpath-for-subpath — the same
verticals, exposed as components and composables.

## Your first viewer

`localEngine()` creates the engine — synchronously, allocating nothing until
first use, so a module-scope `const engine = localEngine()` is safe (even under
SSR). Hand it to `<Viewer>`: the viewer warms it up on mount and PDFium boots
in a worker in the background, so the stage and render layer draw the pages the
moment a document is ready — no worker wiring, no lifecycle to manage. This is
the whole app:

**`App.vue`**

```vue
<script setup lang="ts">
import { onMounted, shallowRef } from 'vue';
import { localEngine } from '@embedpdf/engine';
import type { DocumentHandle, OpenInput } from '@embedpdf/engine';
import PdfPage from './PdfPage.vue';

// The Vue adapter is in progress — this drives the framework-free engine
// directly: App owns the engine and the document, PdfPage renders one page.
const doc = shallowRef<DocumentHandle>();

// The engine is created synchronously and costs nothing until first use —
// only opening a document does real work.
const engine = localEngine();

onMounted(async () => {
  // The local engine opens bytes — fetch the sample and hand them over.
  const ebook: OpenInput = await fetch('https://snippet.embedpdf.com/ebook.pdf')
    .then((response) => response.arrayBuffer())
    .then((buffer) => ({ kind: 'bytes', id: 'ebook', bytes: new Uint8Array(buffer) }));
  doc.value = await engine.open(ebook);
});
</script>

<template>
  <PdfPage v-if="doc" :doc="doc" :page-number="1" />
  <p v-else>Opening document…</p>
</template>
```

**`PdfPage.vue`**

```vue
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import type { DocumentHandle } from '@embedpdf/engine';

const props = defineProps<{ doc: DocumentHandle; pageNumber: number }>();
const src = ref<string>();

onMounted(async () => {
  const { pages } = await props.doc.pages.list();
  const page = pages[props.pageNumber - 1];
  const image = await props.doc
    .page(page.pageObjectNumber)
    .render.image({ viewport: { kind: 'scale', scale: 1.5 } });
  src.value = (await image.objectUrl()).url;
});
</script>

<template>
  <img
    v-if="src"
    :src="src"
    :alt="`Page ${pageNumber}`"
    style="max-width: 100%; border: 1px solid #e6eaf2; border-radius: 8px"
  />
  <p v-else>Rendering page…</p>
</template>
```

Prefer the viewer to own the engine's lifetime — created on mount, destroyed on
unmount? Pass a thunk instead: `engine={() => localEngine()}`. See the
[Engine getting started](https://www.embedpdf.com/docs/engine/getting-started) for the ownership model,
fallback fonts, cloud, and SSR.
