---
title: "Quick start — Svelte"
description: "Build a PDF viewer step by step, with text selection, zoom buttons, a highlighter and a save button."
framework: "Svelte"
source: "https://www.cloudpdf.com/docs/headless/svelte/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/svelte@next @cloudpdf/engine@next
```

Every feature is a separate import from the framework package, such as `@embedpdf/svelte/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.svelte`**

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

  // The engine is created synchronously and costs nothing until first use, so
  // a module-scope `const engine = …` is safe — even under SSR. Only opening a
  // document does real work: the UI renders at t≈0.
  const engine = cloudEngine();
  const plugins = [stagePlugin(), renderPlugin()];

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

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

`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.svelte`**

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

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

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

<Viewer {engine} {plugins} initialDocuments={[{ source: welcome }]}>
  <DocumentGate>
    <Stage style="height: 500px">
      <RenderLayer />
      <SelectionLayer />
    </Stage>
  </DocumentGate>
</Viewer>
```

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 function for its API and one for its state. `useStage()` does things,
and `useStageState()` gives you what to show:

**`zoom-controls.svelte`**

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

  const stage = useStage();
  const state = useStageState();
</script>

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

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.svelte`**

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

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

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

<Viewer {engine} {plugins} initialDocuments={[{ source: welcome }]}>
  <DocumentGate>
    <HighlightButton />
    <Stage style="height: 500px">
      <RenderLayer />
      <SelectionLayer />
      <AnnotationLayer />
    </Stage>
  </DocumentGate>
</Viewer>
```

**`HighlightButton.svelte`**

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

  const interaction = useInteraction();
  const state = useInteractionState();
  const on = $derived(state.activeToolId === 'highlight');
</script>

<button aria-pressed={on} onclick={() => interaction.activateTool(on ? 'pointer' : 'highlight')}>
  Highlight
</button>
```

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.svelte`**

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

  const documents = useDocuments();

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

<button onclick={save}>Save</button>
```

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.
