← Back to blog

Forme 0.11.0 — a Svelte adapter that produces identical PDFs

The Svelte 5 adapter for Forme ships in 0.11.0. Same components, same props, same output — authored as .svelte files, rendered through the same WASM engine.

Forme 0.11.0 adds a Svelte 5 authoring adapter. If you were choosing between "use the React library" and "keep your Svelte stack coherent," you don't have to anymore.

npm install @formepdf/svelte @formepdf/core

The full component set is there: Document, Page, View, Text, semantic headings (H1H6), lists, inline formatting (Strong, Em, Code, Link), tables, media (Image, Svg, QrCode, Barcode, Canvas, Watermark), all five chart types, form fields, and layout primitives (PageBreak, Fixed). Same props, same defaults.

Same components, either language

React:

import { Document, Page, View, Text } from '@formepdf/react';
import { renderDocument } from '@formepdf/core';

const pdf = await renderDocument(
  <Document>
    <Page size="Letter" margin={36}>
      <Text style={{ fontSize: 24, fontWeight: 'bold' }}>Invoice #001</Text>
      <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
        <Text>Widget Pro</Text>
        <Text>$49.00</Text>
      </View>
    </Page>
  </Document>
);

Svelte:

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

<Document>
  <Page size="Letter" margin={36}>
    <Text style={{ fontSize: 24, fontWeight: 'bold' }}>Invoice #001</Text>
    <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
      <Text>Widget Pro</Text>
      <Text>$49.00</Text>
    </View>
  </Page>
</Document>

Byte-identical PDF output. The parity is enforced by a suite of fixture pairs (.svelte + .tsx rendering the same document) that assert the serialized JSON matches — not "matches on the interesting fields," matches exactly.

One-call rendering in SvelteKit

@formepdf/svelte ships renderDocument() and renderDocumentWithLayout() wrappers over the optional @formepdf/core peer, so a SvelteKit endpoint is one line:

// src/routes/invoice/+server.ts
import { renderDocument } from '@formepdf/svelte';
import Invoice from '$lib/Invoice.svelte';

export async function GET({ url }) {
  const pdf = await renderDocument(Invoice, {
    props: { customerName: url.searchParams.get('name') ?? 'Customer' },
  });

  return new Response(pdf, {
    headers: { 'content-type': 'application/pdf' },
  });
}

There is also a formePreview() route helper that gives you the same live-preview UI (layout overlays, click-to-inspect, hot-reload) that the CLI dev server provides for React templates — but as a SvelteKit route you can mount at any path.

How it works

Svelte 5 doesn't expose a walkable component tree at author time the way React does with children props, so a "just walk the JSX" strategy isn't available. The adapter takes a different route: components render namespaced placeholder tags (<forme-document>, <forme-text>, ...) with props as JSON on an attribute, and a parse5-based parser converts the resulting SSR output back into the same document-model JSON the React adapter produces.

The framework-neutral part of that pipeline — style mapping, the Font store, the Canvas recorder, chart kind builders — now lives in a new @formepdf/shared package that both adapters re-export from. @formepdf/react's public API is unchanged; all 215 tests pass without modification.

What's not there yet

Compiled templates (the expression system used by the hosted API — $ref, $each, $if) remain TSX-only. Everything else — component parity, style parity, font handling, WASM rendering, PDF output — is at parity.

Credit

The Svelte adapter was contributed by @cmjoseph07 in PR #21 — a considered, tested, ~9,000-line piece of work covering the adapter, the framework-neutral extraction, parity fixtures, the SvelteKit preview helper, docs, and CI wiring. Thank you.

Get started