> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formepdf.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Svelte

> Author Forme PDFs as Svelte components. The full component set in .svelte files, one-call rendering in SvelteKit endpoints, and a live preview route helper.

`@formepdf/svelte` is the Svelte adapter for Forme.
It ships the same components with the same props as `@formepdf/react`, authored as ordinary `.svelte` files, and serializes to the identical document model.
`{#each}`, `{#if}`, snippets, and text interpolation just work - templates are plain Svelte 5 components evaluated on the server, with no special template language.

## Install

```bash theme={null}
npm install @formepdf/svelte @formepdf/core
```

The adapter requires Svelte 5 (`^5.30.0`) as a peer dependency.
`@formepdf/core` is an *optional* peer: it is only needed to render PDF bytes locally (`renderDocument`, the preview helper).
If you serialize templates and POST the JSON to the hosted API, skip it - `serialize` works with zero WASM.

## Quickstart

Create a template as a normal Svelte component, `src/lib/Invoice.svelte`:

```svelte theme={null}
<script lang="ts">
  import { Document, Page, View, Text } from '@formepdf/svelte';

  interface Item {
    name: string;
    price: number;
  }

  interface Props {
    invoiceNo?: string;
    customer?: string;
    items?: Item[];
  }

  let {
    invoiceNo = '001',
    customer = 'Jane Smith',
    items = [
      { name: 'Website Redesign', price: 3500 },
      { name: 'Hosting (12 months)', price: 600 },
    ],
  }: Props = $props();

  const total = $derived(items.reduce((sum, item) => sum + item.price, 0));
</script>

<Document title="Invoice #{invoiceNo}">
  <Page size="Letter" margin={54}>
    <Text style={{ fontSize: 28, fontWeight: 700, color: '#1e293b' }}>Invoice</Text>

    <View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 24 }}>
      <View>
        <Text style={{ fontSize: 10, color: '#64748b' }}>Bill To</Text>
        <Text style={{ fontSize: 12, fontWeight: 700, marginTop: 4 }}>{customer}</Text>
      </View>
      <Text style={{ fontSize: 10, color: '#64748b' }}>Invoice #{invoiceNo}</Text>
    </View>

    <View style={{ marginTop: 32, padding: 12, backgroundColor: '#f8fafc', borderRadius: 4 }}>
      {#each items as item}
        <View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 8 }}>
          <Text style={{ fontSize: 10 }}>{item.name}</Text>
          <Text style={{ fontSize: 10, fontWeight: 700 }}>${item.price.toFixed(2)}</Text>
        </View>
      {/each}
    </View>

    <View style={{ flexDirection: 'row', justifyContent: 'flex-end', marginTop: 16 }}>
      <Text style={{ fontSize: 14, fontWeight: 700 }}>Total: ${total.toFixed(2)}</Text>
    </View>
  </Page>
</Document>
```

Serve it as a PDF from a SvelteKit endpoint, `src/routes/invoice/+server.ts`:

```ts theme={null}
import { renderDocument } from '@formepdf/svelte';
import Invoice from '$lib/Invoice.svelte';

export async function GET() {
  const pdf = await renderDocument(Invoice, {
    props: { invoiceNo: '001', customer: 'Jane Smith' },
  });
  return new Response(pdf, { headers: { 'Content-Type': 'application/pdf' } });
}
```

That is the whole route.
`renderDocument` serializes the template and renders it through the WASM engine in one call.

Using the hosted API instead?
Serialize without rendering - no `@formepdf/core` install needed:

```ts theme={null}
import { serialize } from '@formepdf/svelte';
import Invoice from '$lib/Invoice.svelte';

const doc = await serialize(Invoice, { props: { invoiceNo: '001' } });
// POST `doc` to the hosted API as JSON
```

## Component parity

The adapter is 1:1 with `@formepdf/react`: the same components with the same props.

* **Layout**: `Document`, `Page`, `View`, `Text`, `Image`, `Fixed`, `PageBreak`
* **Semantics**: `H1`-`H6`, `OrderedList`, `UnorderedList`, `ListItem`, `Strong`, `Em`, `Code`, `Link`
* **Tables**: `Table`, `Row`, `Cell`
* **Graphics**: `Svg`, `QrCode`, `Barcode`, `Canvas`, `Watermark`
* **Charts**: `BarChart`, `LineChart`, `PieChart`, `AreaChart`, `DotPlot`
* **Form fields**: `TextField`, `Checkbox`, `Dropdown`, `RadioButton`

Everything in the [components reference](/components) and [styles reference](/styles) applies verbatim - document-level props (`metadata`, `lang`, `pdfUa`, `pdfa`, `certification`, `fonts`), CSS string shorthands (`border: "1px solid #000"`, `padding: "8 16"`), `StyleSheet.create()`, and the `Style` type are identical.
Only the syntax around the components changes.

Nested `<Text>` spans become styled text runs, so mixed-style lines work exactly as in react:

```svelte theme={null}
<script lang="ts">
  import { Document, Page, Text } from '@formepdf/svelte';

  let { price = 42 }: { price?: number } = $props();
</script>

<Document>
  <Page>
    <Text style={{ fontSize: 12 }}>Was <Text style={{ textDecoration: 'line-through', color: '#999999' }}>$56.00</Text> <Text style={{ fontWeight: 700 }}>${price}.00</Text> due now</Text>
  </Page>
</Document>
```

<Note>
  Compiled templates (`forme build --template`, the `$ref`/`$each`/`$if` expression system for [rendering without a JavaScript runtime](/templates)) are **TSX-only today**.
  Svelte templates always serialize by evaluating the component server-side; to use stored templates with the hosted API, author them in TSX.
</Note>

## Page numbers

The engine substitutes the placeholders `{{pageNumber}}` and `{{totalPages}}` in text at render time.
In JSX the braces can be typed as a string literal (`{'{{pageNumber}}'}`), but in a Svelte template they cannot - Svelte parses `{{pageNumber}}` as an expression containing an object literal, not as text.
Interpolate the exported `PAGE_NUMBER` and `TOTAL_PAGES` constants instead:

```svelte theme={null}
<script lang="ts">
  import { Document, Page, Text, Fixed, PAGE_NUMBER, TOTAL_PAGES } from '@formepdf/svelte';
</script>

<Document>
  <Page>
    <Fixed position="footer" style={{ paddingTop: 8 }}>
      <Text style={{ fontSize: 9, textAlign: 'center' }}>Page {PAGE_NUMBER} of {TOTAL_PAGES}</Text>
    </Fixed>
    <Text>Body content</Text>
  </Page>
</Document>
```

## Fonts

`Font.register()` has the same API as react and feeds the same process-wide store.
Register in a `<script module>` block so registration runs once, not on every render:

```svelte theme={null}
<script module lang="ts">
  import { Font } from '@formepdf/svelte';

  Font.register({ family: 'Inter', src: './fonts/Inter-Regular.ttf' });
  Font.register({ family: 'Inter', src: './fonts/Inter-Bold.ttf', fontWeight: 700 });
</script>

<script lang="ts">
  import { Document, Page, Text } from '@formepdf/svelte';
</script>

<Document style={{ fontFamily: 'Inter' }}>
  <Page size="A4" margin={40}>
    <Text style={{ fontSize: 20, fontWeight: 700 }}>Custom fonts</Text>
    <Text>Registered once, used anywhere.</Text>
  </Page>
</Document>
```

Per-document registration via `<Document fonts={[...]}>` works too.
See the [fonts guide](/fonts) for sources, weights, and fallback chains.

## Tailwind

`@formepdf/tailwind`'s `tw()` works unchanged - it returns a plain style object:

```svelte theme={null}
<script lang="ts">
  import { Document, Page, View, Text } from '@formepdf/svelte';
  import { tw } from '@formepdf/tailwind';
</script>

<Document>
  <Page size="A4" margin={40}>
    <View style={tw('flex-row items-center justify-between p-6 bg-slate-100 rounded-lg')}>
      <Text style={tw('text-2xl font-bold text-slate-900')}>Invoice #001</Text>
      <Text style={tw('text-sm text-slate-500')}>March 2026</Text>
    </View>
  </Page>
</Document>
```

## Custom graphics

`<Canvas>` takes a `draw` callback that records vector operations:

```svelte theme={null}
<script lang="ts">
  import { Document, Page, Canvas } from '@formepdf/svelte';
  import type { CanvasContext } from '@formepdf/svelte';

  function draw(ctx: CanvasContext) {
    ctx.setFillColor(59, 130, 246);
    ctx.circle(50, 50, 40);
    ctx.fill();
  }
</script>

<Document>
  <Page size="A4" margin={40}>
    <Canvas width={100} height={100} {draw} />
  </Page>
</Document>
```

<Note>
  The `draw` callback runs during **server-side serialization**, not at PDF render time.
  It must be synchronous and pure: no `await`, no side effects, no browser or runtime APIs.
</Note>

## Live preview

`formePreview()` gives SvelteKit the same in-browser preview (layout overlays, click-to-inspect) that `forme dev` gives react users, mounted on a catch-all dev route.
Create `src/routes/dev/pdf/[...forme]/+server.ts`:

```ts theme={null}
import { formePreview } from '@formepdf/svelte/preview';
import Invoice from '$lib/Invoice.svelte';

export const GET = formePreview(Invoice, {
  props: { invoiceNo: '001' },
});
```

Open `/dev/pdf` in the browser.
The preview polls for changes and reloads shortly after you save the template (default 1000 ms in dev; polling is disabled when `NODE_ENV` is `production`, and the interval is configurable via `pollMs`).
The helper renders through `@formepdf/core`, so the optional peer must be installed.

## Render options

`renderDocument` and `renderDocumentWithLayout` forward all `@formepdf/core` render options unchanged, so features like [embedded data](/embedded-data) and form flattening work exactly as documented for react:

```ts theme={null}
import { renderDocument } from '@formepdf/svelte';
import Invoice from '$lib/Invoice.svelte';

const pdf = await renderDocument(Invoice, {
  props: { invoiceNo: '001' },
  embedData: { invoiceNo: '001', total: 4100 },
  flattenForms: true,
});
```

## Migrating from TSX

Migration is mechanical: component names, props, and style objects are identical, so most of a template moves over as-is.
The react [quickstart](/quickstart) maps to Svelte like this:

| TSX                                         | Svelte                                                       |
| ------------------------------------------- | ------------------------------------------------------------ |
| `export default (<Document>...)`            | A `.svelte` file whose markup is `<Document>...`             |
| `function Invoice(data) { ... }`            | `let { ... } = $props()`                                     |
| `{items.map(item => (<View>...</View>))}`   | `{#each items as item}<View>...</View>{/each}`               |
| `{showFooter && <Text>...</Text>}`          | `{#if showFooter}<Text>...</Text>{/if}`                      |
| `{'{{pageNumber}}'}`                        | `{PAGE_NUMBER}`                                              |
| `renderDocument(doc)` from `@formepdf/core` | `renderDocument(Invoice, { props })` from `@formepdf/svelte` |

Two differences to note.
First, react's `renderDocument` takes an *element* (`<Document>...`), while Svelte components cannot be pre-bound to props - so the Svelte adapter's `renderDocument`, `serialize`, and `render` take the component plus a `props` option.
Second, whitespace in `.svelte` templates is normalized to the same rules JSX uses, so indentation never leaks stray spaces into rendered text.
