---
title: "Getting Started — Svelte"
description: "Build your own PDF viewer UI with the EmbedPDF headless libraries."
framework: "Svelte"
source: "https://www.embedpdf.com/docs/headless/svelte/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/svelte @embedpdf/engine
```

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

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

```svelte
<script lang="ts">
  import { onMount } from 'svelte';
  import type { LocalEngine } from '@embedpdf/engine';
  import PdfPage from './PdfPage.svelte';

  // The Svelte adapter is in progress — this drives the framework-free engine
  // directly: App owns the engine and the document, PdfPage renders one page.
  type PdfDocument = Awaited<ReturnType<LocalEngine['open']>>;

  let doc = $state<PdfDocument>();
  let status = $state('Booting engine…');

  onMount(async () => {
    // `localEngine()` IS the engine — no worker wiring needed; PDFium boots in
    // a worker on first use. The Svelte adapter will own this for you.
    const { localEngine } = await import('@embedpdf/engine');
    const engine = localEngine();

    status = 'Opening document…';
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    const bytes = new Uint8Array(await response.arrayBuffer());
    doc = await engine.open({ kind: 'bytes', id: 'ebook', bytes });
  });
</script>

{#if doc}
  <PdfPage {doc} pageNumber={1} />
{:else}
  <p>{status}</p>
{/if}
```

**`PdfPage.svelte`**

```svelte
<script lang="ts">
  import { onMount } from 'svelte';
  import type { LocalEngine } from '@embedpdf/engine';

  type PdfDocument = Awaited<ReturnType<LocalEngine['open']>>;

  let { doc, pageNumber }: { doc: PdfDocument; pageNumber: number } = $props();

  let src = $state<string>();

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

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

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.
