Forme is a document engine for JavaScript, written in Rust and compiled to WASM. Render the HTML and print CSS you already have, or author React, Svelte, Vue, and Preact components. Paginated PDFs in-process - no headless browser.
See the codeForme is a document engine for JavaScript, written in Rust and compiled to WASM. Render the HTML and print CSS you already have, or author React, Svelte, Vue, and Preact components. Paginated PDFs in-process - no headless browser.

Every PDF tool makes you choose: fight with CSS page breaks or use an editor that can't handle dynamic data. Forme is a layout engine built for pages. No headless browser. No Chrome. Renders in milliseconds. Runs anywhere — Node, the browser, or at the edge.
Have HTML and print CSS already? Render it directly — no browser involved:
npx @formepdf/html invoice.html -o invoice.pdf
@page rules, margin boxes, counter(page), and break-before all work; anything outside the documented subset warns by name instead of failing silently. Also usable as a library (renderHtml() from @formepdf/html) in Node, the browser, and Cloudflare Workers.
npm install @formepdf/cli @formepdf/react @formepdf/core
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 #2024-001</Text>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 24 }}>
<Text>Widget Pro</Text>
<Text>$49.00</Text>
</View>
</Page>
</Document>
);
// pdf is a Uint8Array: save it, serve it, email it
Same components, same props, authored as .svelte files:
npm install @formepdf/svelte @formepdf/core
<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 #2024-001</Text>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 24 }}>
<Text>Widget Pro</Text>
<Text>$49.00</Text>
</View>
</Page>
</Document>
See the Svelte docs for renderDocument, the SvelteKit preview route helper, and endpoint patterns.
Same components, same props, authored with Preact's JSX runtime:
npm install @formepdf/preact @formepdf/core
/** @jsxImportSource preact */
import { Document, Page, View, Text, renderDocument } from '@formepdf/preact';
const pdf = await renderDocument(
<Document>
<Page size="Letter" margin={36}>
<Text style={{ fontSize: 24, fontWeight: 'bold' }}>Invoice #2024-001</Text>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 24 }}>
<Text>Widget Pro</Text>
<Text>$49.00</Text>
</View>
</Page>
</Document>
);
See the Preact docs for setup notes and edge/Workers examples.
Same components, same props, authored as ordinary .vue single-file components — v-for, v-if, slots, and {{ }} interpolation all work:
npm install @formepdf/vue @formepdf/core
<script setup lang="ts">
import { Document, Page, View, Text } from '@formepdf/vue';
</script>
<template>
<Document>
<Page size="Letter" :margin="36">
<Text :style="{ fontSize: 24, fontWeight: 'bold' }">Invoice #2024-001</Text>
<View :style="{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 24 }">
<Text>Widget Pro</Text>
<Text>$49.00</Text>
</View>
</Page>
</Document>
</template>
See the Vue docs for the compiler option one-liner and Nitro/Nuxt endpoint patterns.
npx forme dev invoice.tsx --data sample.json
Live preview with debug overlays. Click any element to inspect its computed styles.
Install Forme PDF Preview from the VS Code Marketplace.
@formepdf/react), .svelte files (@formepdf/svelte), or Preact JSX (@formepdf/preact) — identical props, identical output.forme dev shows your PDF updating in real time as you edit.hyphens: 'auto' and a lang tag. Uses the hypher crate with language-specific dictionaries.direction: 'rtl' or direction: 'auto'.display: 'grid'. Fixed, fractional (fr), and auto track sizing. Explicit placement, auto-placement, column/row spanning, and row-level page breaks.align-content distributes wrapped lines (center, space-between, space-around, space-evenly, flex-end, stretch).minWidowLines and minOrphanLines.position: 'absolute' with top, right, bottom, left relative to the parent View.justifyContent and alignItems work in both row and column directions.rect, circle, ellipse, line, polyline, polygon, and path elements. Supports opacity, fill-opacity, and stroke-opacity. Pass SVG as a content string or as JSX children.<QrCode> component. Vector-based, crisp at any zoom level.<Barcode> component. Code 128, Code 39, EAN-13, EAN-8, Codabar. Vector-based.textOverflow: 'ellipsis' truncates single-line text with "..." when it exceeds available width. Also supports 'clip'.fontFamily: "Inter, Helvetica" tries each font in order, falling back automatically.href to any <Text>, <View>, <Image>, or <Svg> for clickable PDF links.bookmark to any element for PDF outline entries. Navigate long documents from the bookmark panel.<Text> inside <Text> to bold a word, change colors mid-sentence, or apply strikethrough.alt text for accessibility.border: "1px solid #000", padding: "8 16", margin: [20, 40] — CSS-style shorthand strings and arrays parse automatically.opacity cascades to children, wordSpacing, boxShadow, ubiquitous borderRadius (rounded clipping when overflow: hidden), and background accepting CSS gradient strings — linear-gradient(135deg, #667eea, #764ba2), radial-gradient(circle, #10b981, #059669). Multi-stop gradients supported.<Page backgroundImage="..." backgroundSize="cover" backgroundOpacity={0.08} /> for watermark-style overlays. Sizes: fill / cover / contain.<Document lang="en-US"> sets the PDF /Lang tag for accessibility.{{pageNumber}} and {{totalPages}} in any text element.@formepdf/core/browser to generate PDFs entirely client-side. Same engine, same templates — no server required.tw("p-4 text-lg font-bold bg-blue-500") converts Tailwind classes to Forme style objects. Full color palette, grid, arbitrary values, negative values, fractions.<TextField>, <Checkbox>, <Dropdown>, <RadioButton>. Fill and flatten for non-editable delivery.<Document pdfUa> generates PDF/UA-1 conforming documents — structure tree with tagged headings, lists (/LBody), tables (/TH scope, /ColSpan), links (/Link + OBJR), figure /Alt, tab order, and artifact tagging. See Compliance.<Document pdfa="2b"> for long-term preservation — PDF/A-2b, 2u, and 2a, veraPDF-verified, and composable with pdfUa (archival + accessible at once). See Compliance.certification prop or /v1/certify API endpoint./v1/merge./v1/rasterize, powered by PDFium.PDF/UA-1 (accessibility) — verified. A nine-document corpus — the five
shipped @formepdf/templates (invoice, receipt, report,
shipping-label, letter) and four HTML fixtures (letterhead, dashed-borders,
statement, zebra-invoice) — passes veraPDF 1.30.2 against
the PDF/UA-1 profile: 9/9. The check runs in CI on every push
(scripts/verify-pdfua.mjs), so the claim can't silently rot. The live results
— every file, both standards, regenerated from CI on each commit — are at
parity.formepdf.com.
tagged={false} to opt out. Tagging is layout-neutral — the tag tree is built
after layout, so geometry and visual output are byte-for-byte identical.pdfUa, give the document a
lang, and register a metric-compatible font so the base-14 families embed —
install @formepdf/fonts-standard and pass
standardFonts() to fonts. It's a separate, optional package: core carries
no font payload, so users who don't need conformance don't pay for it.pdfUa is set but no embeddable font is
registered, the render still succeeds and names the gap in warnings (surfaced
through renderPdfWithLayout and the HTML wrapper) rather than emitting a PDF
that falsely claims conformance.import { standardFonts } from '@formepdf/fonts-standard';
<Document pdfUa lang="en-US" fonts={standardFonts()}>
{/* … */}
</Document>
PDF/A (archival) — verified. <Document pdfa="2b">, "2u", and "2a" produce
PDF/A-2 conforming files; "3b", "3u", and "3a" produce PDF/A-3 (identical
rules plus permission for arbitrary embedded files — the e-invoice container
part). Verified by the same veraPDF gate: the nine-document corpus passes
PDF/A-2b, 2a, 3b, and 3a. The font path uses the same
metric-compatible embedding as PDF/UA (with a per-glyph width carve-out for the
few glyphs where Liberation's advances diverge from the base-14 AFM metrics), an
embedded sRGB OutputIntent, and PDF/A XMP metadata.
Archival and accessible. PDF/A composes with PDF/UA — set both and the file is conformant to each at once:
<Document pdfa="2a" pdfUa lang="en-US" fonts={standardFonts()}>…</Document>
The CI gate validates every corpus file against PDF/A-2b, 2a, 3b, and 3a, and
PDF/UA-1 together (scripts/verify-pdfa.mjs). The HTML path takes the same via
pdfA (renderHtml) / --pdf-a (CLI). Needs an embeddable font
(@formepdf/fonts-standard) — if none is
registered, the render fails by name rather than emitting a file that falsely
claims conformance. See Archival.
E-invoice containers (Factur-X / ZUGFeRD) — verified. One render call
produces the human-readable invoice PDF and embeds your EN 16931 invoice XML
as a conformant PDF/A-3 associated file — spec filename, MIME type,
/AFRelationship, and the Factur-X XMP identification included:
const pdf = await renderDocument(
<Document pdfa="3b" pdfUa lang="de-DE" fonts={standardFonts()}>…</Document>,
{ facturX: { xml: invoiceXml, profile: 'EN 16931' } },
);
Every existing JS e-invoicing package requires you to bring the visual PDF from
somewhere else; Forme renders it and builds the container in one pass. CI gates
the result with both validators — veraPDF (PDF/A-3b + PDF/UA-1) and
Mustangproject, the ZUGFeRD/Factur-X reference validator
(scripts/verify-einvoice.mjs; evidence on the parity page).
The boundary, stated plainly: Forme produces the conformant container. It
does not generate or validate EN 16931 semantic content — the invoice XML
is yours (from your ERP, or a library like the CEN artefacts). MINIMUM and
BASIC WL profiles are not legally e-invoices under the German B2B mandate;
supply an EN 16931-profile XML for that. See
E-invoicing.
Generate PDFs in the browser with zero server dependencies:
import { renderDocument } from '@formepdf/core/browser';
import { Document, Page, Text } from '@formepdf/react';
const pdfBytes = await renderDocument(
<Document>
<Page size="Letter" margin={36}>
<Text style={{ fontSize: 24 }}>Generated in the browser</Text>
</Page>
</Document>
);
// Download, display in an iframe, or upload
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
window.open(url);
Works with Vite, Next.js, Remix, or any bundler that handles WASM. The only difference from server-side usage is the import path.
Register TrueType fonts globally or per-document:
import { Font, Document, Text } from '@formepdf/react';
import { renderDocument } from '@formepdf/core';
// Global registration (works like react-pdf)
Font.register({
family: 'Inter',
src: './fonts/Inter-Regular.ttf',
});
Font.register({
family: 'Inter',
src: './fonts/Inter-Bold.ttf',
fontWeight: 'bold',
});
const pdf = await renderDocument(
<Document>
<Text style={{ fontFamily: 'Inter', fontSize: 16 }}>
Regular text
</Text>
<Text style={{ fontFamily: 'Inter', fontSize: 16, fontWeight: 'bold' }}>
Bold text
</Text>
</Document>
);
Or pass fonts directly on the Document:
<Document fonts={[
{ family: 'Roboto', src: './fonts/Roboto-Regular.ttf' },
{ family: 'Roboto', src: './fonts/Roboto-Italic.ttf', fontStyle: 'italic' },
]}>
Font sources can be file paths, data URIs, or Uint8Array. Fonts are automatically subsetted — only glyphs used in the document are embedded.
| Component | Description |
|---|---|
<Document> | Root element. title, author, lang, fonts, style. |
<Page> | A page. size, margin (number, string, array, or edges). |
<View> | Container. Flexbox layout. href, bookmark. |
<Text> | Text content. Fonts, sizes, colors. href, bookmark. |
<Image> | JPEG or PNG. href, alt. Aspect ratio preserved. |
<Table> | Table with column definitions. |
<Row> | Table row. header for repeating on page breaks. |
<Cell> | Table cell. colSpan, rowSpan. |
<Svg> | Inline SVG graphics. content string or JSX children. href, alt. |
<QrCode> | QR code. data, size, color. Vector-based. |
<Barcode> | 1D barcode. data, format, width, height, color. Code 128, Code 39, EAN-13, EAN-8, Codabar. |
<Canvas> | Arbitrary vector drawing via draw callback. |
<BarChart> | Bar chart. data, color, showGrid, showValues, title. |
<LineChart> | Multi-series line chart. series, labels, showPoints, showGrid, title. |
<PieChart> | Pie/donut chart. data, donut, showLegend, title. |
<AreaChart> | Multi-series area chart. series, labels, showGrid, title. |
<DotPlot> | Scatter plot. groups, xLabel, yLabel, showLegend, dotSize. |
<Watermark> | Rotated text behind page content. text, fontSize, color, angle. |
<TextField> | Form text input. name, value, width, multiline, password, readOnly. |
<Checkbox> | Form checkbox. name, checked. |
<Dropdown> | Form select dropdown. name, options, value, width. |
<RadioButton> | Form radio button. name, value, checked. |
<Fixed> | Repeating header or footer. |
<PageBreak> | Force a page break. |
| Forme | react-pdf | Puppeteer | |
|---|---|---|---|
| Page breaks | Page-native layout (widows/orphans) | Widows/orphans (recent rewrite) | CSS page-break (fragile) |
| Table header repetition | Automatic on every page | Not built in | Inconsistent <thead> |
| Line breaking | Knuth-Plass | Knuth-Plass | Browser engine |
| Hyphenation | Automatic, 35+ languages bundled | Automatic (en-US bundled) | Browser engine |
| Text shaping | OpenType GSUB/GPOS (rustybuzz) | OpenType GSUB/GPOS (fontkit) | Full browser shaping |
| BiDi text | RTL, mixed LTR/RTL (unicode-bidi) | RTL, mixed LTR/RTL (bidi-js) | Full browser BiDi |
| CSS Grid | display: 'grid' with fr/auto/fixed tracks | No | Full CSS Grid |
| Live preview | Built-in dev server | Render to file | Run script, open file |
| Click-to-inspect | VS Code, Cursor, WebStorm | No | No |
| Render speed (warm) | ~20ms (6-page report) — measured | ~100-500ms | ~54ms warm / ~370ms cold |
| Cold start (→ first PDF byte) | ~64ms (Workers) / ~110ms (Node) | — | 3-10s cold serverless; can't boot on many tiers |
| Memory per render | ~7MB (1p) → >1GB (500p) | ~50-100MB | ~50-200MB |
| SVG | Basic shapes and paths | Yes | Full browser SVG |
| Links | href prop on Text/View/Image/Svg | <Link> component | HTML <a> tags |
| Bookmarks | bookmark prop on any element | Yes | No |
| QR codes | Built-in <QrCode> component | No | Via HTML/JS libraries |
| Barcodes | Built-in <Barcode> (5 formats) | No | Via HTML/JS libraries |
| Charts | Engine-native BarChart, LineChart, PieChart, AreaChart, DotPlot | No | Via HTML/JS libraries |
| VS Code extension | Native sidebar panels | No | No |
| Canvas drawing | <Canvas draw={...}> for custom vector graphics | <Canvas> primitive | HTML Canvas (raster) |
| Watermarks | Built-in <Watermark> component | No | Manual positioning |
| Embedded data | Attach JSON to PDF, extract later | No | No |
| Text overflow | textOverflow: 'ellipsis' | textOverflow + maxLines | CSS text-overflow |
| Font fallback | fontFamily array + per-glyph fallback | fontFamily array | Full CSS font stack |
| Custom fonts | TTF with OpenType shaping | Yes | Yes |
| Browser rendering | Yes (@formepdf/core/browser) | Yes (client-side) | No (server only) |
| Tailwind CSS | tw("p-4 text-lg font-bold") utility | No | No |
| Fillable forms | AcroForm (TextField, Checkbox, Dropdown, Radio) | AcroForm primitives (TextInput, Select, Checkbox, FieldSet) | HTML <form> (not PDF forms) |
| PDF/UA accessibility | <Document pdfUa> | No | No |
| PDF/A archival | <Document pdfa="2b"> | No | No |
| Digital certification | PKCS#7 via certification prop or API | No | No |
| Redaction | True content removal + metadata scrubbing | No | No |
| PDF merging | Combine multiple PDFs | No | No |
| Rasterization | PDF → PNG via PDFium | No | No |
| Dependencies | None (WASM) | yoga-layout | Chrome/Chromium |
| Runs in-process | Yes | Yes | No (subprocess) |
See the templates/ directory for production-ready examples:
Style Forme components with Tailwind utility classes via @formepdf/tailwind:
npm install @formepdf/tailwind
import { tw } from '@formepdf/tailwind';
<View style={tw("flex-row items-center gap-4 p-6 bg-slate-100 rounded-lg")}>
<Text style={tw("text-2xl font-bold text-slate-900")}>Invoice</Text>
<Text style={tw("text-sm text-slate-500")}>Draft</Text>
</View>
Supports spacing, typography, colors (full Tailwind palette), flexbox, grid, borders, opacity, arbitrary values (w-[200], bg-[#f00]), negative values (-mt-4), fraction widths (w-1/2), and self-* alignment.
Full docs at docs.formepdf.com:
I'd like to hear about it: what you're generating, what you used before, and roughly how many documents a month. It directly shapes what gets built next. There's a short form at formepdf.com/production.
Issues and PRs welcome — see CONTRIBUTING.md for setup, the per-area test commands, and the house rules (fail-loud on unsupported CSS, keep the subset docs in sync, propose engine changes first). Security reports go through SECURITY.md.
MIT
1,409 followers · starred Apr 2026
261 followers · starred Sep 2026
23 followers · starred Mar 2026
117 followers · starred Apr 2026
Forme is a document engine for JavaScript, written in Rust and compiled to WASM. Render the HTML and print CSS you already have, or author React, Svelte, Vue, and Preact components. Paginated PDFs in-process - no headless browser.
See the codeForme is a document engine for JavaScript, written in Rust and compiled to WASM. Render the HTML and print CSS you already have, or author React, Svelte, Vue, and Preact components. Paginated PDFs in-process - no headless browser.

Every PDF tool makes you choose: fight with CSS page breaks or use an editor that can't handle dynamic data. Forme is a layout engine built for pages. No headless browser. No Chrome. Renders in milliseconds. Runs anywhere — Node, the browser, or at the edge.
Have HTML and print CSS already? Render it directly — no browser involved:
npx @formepdf/html invoice.html -o invoice.pdf
@page rules, margin boxes, counter(page), and break-before all work; anything outside the documented subset warns by name instead of failing silently. Also usable as a library (renderHtml() from @formepdf/html) in Node, the browser, and Cloudflare Workers.
npm install @formepdf/cli @formepdf/react @formepdf/core
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 #2024-001</Text>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 24 }}>
<Text>Widget Pro</Text>
<Text>$49.00</Text>
</View>
</Page>
</Document>
);
// pdf is a Uint8Array: save it, serve it, email it
Same components, same props, authored as .svelte files:
npm install @formepdf/svelte @formepdf/core
<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 #2024-001</Text>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 24 }}>
<Text>Widget Pro</Text>
<Text>$49.00</Text>
</View>
</Page>
</Document>
See the Svelte docs for renderDocument, the SvelteKit preview route helper, and endpoint patterns.
Same components, same props, authored with Preact's JSX runtime:
npm install @formepdf/preact @formepdf/core
/** @jsxImportSource preact */
import { Document, Page, View, Text, renderDocument } from '@formepdf/preact';
const pdf = await renderDocument(
<Document>
<Page size="Letter" margin={36}>
<Text style={{ fontSize: 24, fontWeight: 'bold' }}>Invoice #2024-001</Text>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 24 }}>
<Text>Widget Pro</Text>
<Text>$49.00</Text>
</View>
</Page>
</Document>
);
See the Preact docs for setup notes and edge/Workers examples.
Same components, same props, authored as ordinary .vue single-file components — v-for, v-if, slots, and {{ }} interpolation all work:
npm install @formepdf/vue @formepdf/core
<script setup lang="ts">
import { Document, Page, View, Text } from '@formepdf/vue';
</script>
<template>
<Document>
<Page size="Letter" :margin="36">
<Text :style="{ fontSize: 24, fontWeight: 'bold' }">Invoice #2024-001</Text>
<View :style="{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 24 }">
<Text>Widget Pro</Text>
<Text>$49.00</Text>
</View>
</Page>
</Document>
</template>
See the Vue docs for the compiler option one-liner and Nitro/Nuxt endpoint patterns.
npx forme dev invoice.tsx --data sample.json
Live preview with debug overlays. Click any element to inspect its computed styles.
Install Forme PDF Preview from the VS Code Marketplace.
@formepdf/react), .svelte files (@formepdf/svelte), or Preact JSX (@formepdf/preact) — identical props, identical output.forme dev shows your PDF updating in real time as you edit.hyphens: 'auto' and a lang tag. Uses the hypher crate with language-specific dictionaries.direction: 'rtl' or direction: 'auto'.display: 'grid'. Fixed, fractional (fr), and auto track sizing. Explicit placement, auto-placement, column/row spanning, and row-level page breaks.align-content distributes wrapped lines (center, space-between, space-around, space-evenly, flex-end, stretch).minWidowLines and minOrphanLines.position: 'absolute' with top, right, bottom, left relative to the parent View.justifyContent and alignItems work in both row and column directions.rect, circle, ellipse, line, polyline, polygon, and path elements. Supports opacity, fill-opacity, and stroke-opacity. Pass SVG as a content string or as JSX children.<QrCode> component. Vector-based, crisp at any zoom level.<Barcode> component. Code 128, Code 39, EAN-13, EAN-8, Codabar. Vector-based.textOverflow: 'ellipsis' truncates single-line text with "..." when it exceeds available width. Also supports 'clip'.fontFamily: "Inter, Helvetica" tries each font in order, falling back automatically.href to any <Text>, <View>, <Image>, or <Svg> for clickable PDF links.bookmark to any element for PDF outline entries. Navigate long documents from the bookmark panel.<Text> inside <Text> to bold a word, change colors mid-sentence, or apply strikethrough.alt text for accessibility.border: "1px solid #000", padding: "8 16", margin: [20, 40] — CSS-style shorthand strings and arrays parse automatically.opacity cascades to children, wordSpacing, boxShadow, ubiquitous borderRadius (rounded clipping when overflow: hidden), and background accepting CSS gradient strings — linear-gradient(135deg, #667eea, #764ba2), radial-gradient(circle, #10b981, #059669). Multi-stop gradients supported.<Page backgroundImage="..." backgroundSize="cover" backgroundOpacity={0.08} /> for watermark-style overlays. Sizes: fill / cover / contain.<Document lang="en-US"> sets the PDF /Lang tag for accessibility.{{pageNumber}} and {{totalPages}} in any text element.@formepdf/core/browser to generate PDFs entirely client-side. Same engine, same templates — no server required.tw("p-4 text-lg font-bold bg-blue-500") converts Tailwind classes to Forme style objects. Full color palette, grid, arbitrary values, negative values, fractions.<TextField>, <Checkbox>, <Dropdown>, <RadioButton>. Fill and flatten for non-editable delivery.<Document pdfUa> generates PDF/UA-1 conforming documents — structure tree with tagged headings, lists (/LBody), tables (/TH scope, /ColSpan), links (/Link + OBJR), figure /Alt, tab order, and artifact tagging. See Compliance.<Document pdfa="2b"> for long-term preservation — PDF/A-2b, 2u, and 2a, veraPDF-verified, and composable with pdfUa (archival + accessible at once). See Compliance.certification prop or /v1/certify API endpoint./v1/merge./v1/rasterize, powered by PDFium.PDF/UA-1 (accessibility) — verified. A nine-document corpus — the five
shipped @formepdf/templates (invoice, receipt, report,
shipping-label, letter) and four HTML fixtures (letterhead, dashed-borders,
statement, zebra-invoice) — passes veraPDF 1.30.2 against
the PDF/UA-1 profile: 9/9. The check runs in CI on every push
(scripts/verify-pdfua.mjs), so the claim can't silently rot. The live results
— every file, both standards, regenerated from CI on each commit — are at
parity.formepdf.com.
tagged={false} to opt out. Tagging is layout-neutral — the tag tree is built
after layout, so geometry and visual output are byte-for-byte identical.pdfUa, give the document a
lang, and register a metric-compatible font so the base-14 families embed —
install @formepdf/fonts-standard and pass
standardFonts() to fonts. It's a separate, optional package: core carries
no font payload, so users who don't need conformance don't pay for it.pdfUa is set but no embeddable font is
registered, the render still succeeds and names the gap in warnings (surfaced
through renderPdfWithLayout and the HTML wrapper) rather than emitting a PDF
that falsely claims conformance.import { standardFonts } from '@formepdf/fonts-standard';
<Document pdfUa lang="en-US" fonts={standardFonts()}>
{/* … */}
</Document>
PDF/A (archival) — verified. <Document pdfa="2b">, "2u", and "2a" produce
PDF/A-2 conforming files; "3b", "3u", and "3a" produce PDF/A-3 (identical
rules plus permission for arbitrary embedded files — the e-invoice container
part). Verified by the same veraPDF gate: the nine-document corpus passes
PDF/A-2b, 2a, 3b, and 3a. The font path uses the same
metric-compatible embedding as PDF/UA (with a per-glyph width carve-out for the
few glyphs where Liberation's advances diverge from the base-14 AFM metrics), an
embedded sRGB OutputIntent, and PDF/A XMP metadata.
Archival and accessible. PDF/A composes with PDF/UA — set both and the file is conformant to each at once:
<Document pdfa="2a" pdfUa lang="en-US" fonts={standardFonts()}>…</Document>
The CI gate validates every corpus file against PDF/A-2b, 2a, 3b, and 3a, and
PDF/UA-1 together (scripts/verify-pdfa.mjs). The HTML path takes the same via
pdfA (renderHtml) / --pdf-a (CLI). Needs an embeddable font
(@formepdf/fonts-standard) — if none is
registered, the render fails by name rather than emitting a file that falsely
claims conformance. See Archival.
E-invoice containers (Factur-X / ZUGFeRD) — verified. One render call
produces the human-readable invoice PDF and embeds your EN 16931 invoice XML
as a conformant PDF/A-3 associated file — spec filename, MIME type,
/AFRelationship, and the Factur-X XMP identification included:
const pdf = await renderDocument(
<Document pdfa="3b" pdfUa lang="de-DE" fonts={standardFonts()}>…</Document>,
{ facturX: { xml: invoiceXml, profile: 'EN 16931' } },
);
Every existing JS e-invoicing package requires you to bring the visual PDF from
somewhere else; Forme renders it and builds the container in one pass. CI gates
the result with both validators — veraPDF (PDF/A-3b + PDF/UA-1) and
Mustangproject, the ZUGFeRD/Factur-X reference validator
(scripts/verify-einvoice.mjs; evidence on the parity page).
The boundary, stated plainly: Forme produces the conformant container. It
does not generate or validate EN 16931 semantic content — the invoice XML
is yours (from your ERP, or a library like the CEN artefacts). MINIMUM and
BASIC WL profiles are not legally e-invoices under the German B2B mandate;
supply an EN 16931-profile XML for that. See
E-invoicing.
Generate PDFs in the browser with zero server dependencies:
import { renderDocument } from '@formepdf/core/browser';
import { Document, Page, Text } from '@formepdf/react';
const pdfBytes = await renderDocument(
<Document>
<Page size="Letter" margin={36}>
<Text style={{ fontSize: 24 }}>Generated in the browser</Text>
</Page>
</Document>
);
// Download, display in an iframe, or upload
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
window.open(url);
Works with Vite, Next.js, Remix, or any bundler that handles WASM. The only difference from server-side usage is the import path.
Register TrueType fonts globally or per-document:
import { Font, Document, Text } from '@formepdf/react';
import { renderDocument } from '@formepdf/core';
// Global registration (works like react-pdf)
Font.register({
family: 'Inter',
src: './fonts/Inter-Regular.ttf',
});
Font.register({
family: 'Inter',
src: './fonts/Inter-Bold.ttf',
fontWeight: 'bold',
});
const pdf = await renderDocument(
<Document>
<Text style={{ fontFamily: 'Inter', fontSize: 16 }}>
Regular text
</Text>
<Text style={{ fontFamily: 'Inter', fontSize: 16, fontWeight: 'bold' }}>
Bold text
</Text>
</Document>
);
Or pass fonts directly on the Document:
<Document fonts={[
{ family: 'Roboto', src: './fonts/Roboto-Regular.ttf' },
{ family: 'Roboto', src: './fonts/Roboto-Italic.ttf', fontStyle: 'italic' },
]}>
Font sources can be file paths, data URIs, or Uint8Array. Fonts are automatically subsetted — only glyphs used in the document are embedded.
| Component | Description |
|---|---|
<Document> | Root element. title, author, lang, fonts, style. |
<Page> | A page. size, margin (number, string, array, or edges). |
<View> | Container. Flexbox layout. href, bookmark. |
<Text> | Text content. Fonts, sizes, colors. href, bookmark. |
<Image> | JPEG or PNG. href, alt. Aspect ratio preserved. |
<Table> | Table with column definitions. |
<Row> | Table row. header for repeating on page breaks. |
<Cell> | Table cell. colSpan, rowSpan. |
<Svg> | Inline SVG graphics. content string or JSX children. href, alt. |
<QrCode> | QR code. data, size, color. Vector-based. |
<Barcode> | 1D barcode. data, format, width, height, color. Code 128, Code 39, EAN-13, EAN-8, Codabar. |
<Canvas> | Arbitrary vector drawing via draw callback. |
<BarChart> | Bar chart. data, color, showGrid, showValues, title. |
<LineChart> | Multi-series line chart. series, labels, showPoints, showGrid, title. |
<PieChart> | Pie/donut chart. data, donut, showLegend, title. |
<AreaChart> | Multi-series area chart. series, labels, showGrid, title. |
<DotPlot> | Scatter plot. groups, xLabel, yLabel, showLegend, dotSize. |
<Watermark> | Rotated text behind page content. text, fontSize, color, angle. |
<TextField> | Form text input. name, value, width, multiline, password, readOnly. |
<Checkbox> | Form checkbox. name, checked. |
<Dropdown> | Form select dropdown. name, options, value, width. |
<RadioButton> | Form radio button. name, value, checked. |
<Fixed> | Repeating header or footer. |
<PageBreak> | Force a page break. |
| Forme | react-pdf | Puppeteer | |
|---|---|---|---|
| Page breaks | Page-native layout (widows/orphans) | Widows/orphans (recent rewrite) | CSS page-break (fragile) |
| Table header repetition | Automatic on every page | Not built in | Inconsistent <thead> |
| Line breaking | Knuth-Plass | Knuth-Plass | Browser engine |
| Hyphenation | Automatic, 35+ languages bundled | Automatic (en-US bundled) | Browser engine |
| Text shaping | OpenType GSUB/GPOS (rustybuzz) | OpenType GSUB/GPOS (fontkit) | Full browser shaping |
| BiDi text | RTL, mixed LTR/RTL (unicode-bidi) | RTL, mixed LTR/RTL (bidi-js) | Full browser BiDi |
| CSS Grid | display: 'grid' with fr/auto/fixed tracks | No | Full CSS Grid |
| Live preview | Built-in dev server | Render to file | Run script, open file |
| Click-to-inspect | VS Code, Cursor, WebStorm | No | No |
| Render speed (warm) | ~20ms (6-page report) — measured | ~100-500ms | ~54ms warm / ~370ms cold |
| Cold start (→ first PDF byte) | ~64ms (Workers) / ~110ms (Node) | — | 3-10s cold serverless; can't boot on many tiers |
| Memory per render | ~7MB (1p) → >1GB (500p) | ~50-100MB | ~50-200MB |
| SVG | Basic shapes and paths | Yes | Full browser SVG |
| Links | href prop on Text/View/Image/Svg | <Link> component | HTML <a> tags |
| Bookmarks | bookmark prop on any element | Yes | No |
| QR codes | Built-in <QrCode> component | No | Via HTML/JS libraries |
| Barcodes | Built-in <Barcode> (5 formats) | No | Via HTML/JS libraries |
| Charts | Engine-native BarChart, LineChart, PieChart, AreaChart, DotPlot | No | Via HTML/JS libraries |
| VS Code extension | Native sidebar panels | No | No |
| Canvas drawing | <Canvas draw={...}> for custom vector graphics | <Canvas> primitive | HTML Canvas (raster) |
| Watermarks | Built-in <Watermark> component | No | Manual positioning |
| Embedded data | Attach JSON to PDF, extract later | No | No |
| Text overflow | textOverflow: 'ellipsis' | textOverflow + maxLines | CSS text-overflow |
| Font fallback | fontFamily array + per-glyph fallback | fontFamily array | Full CSS font stack |
| Custom fonts | TTF with OpenType shaping | Yes | Yes |
| Browser rendering | Yes (@formepdf/core/browser) | Yes (client-side) | No (server only) |
| Tailwind CSS | tw("p-4 text-lg font-bold") utility | No | No |
| Fillable forms | AcroForm (TextField, Checkbox, Dropdown, Radio) | AcroForm primitives (TextInput, Select, Checkbox, FieldSet) | HTML <form> (not PDF forms) |
| PDF/UA accessibility | <Document pdfUa> | No | No |
| PDF/A archival | <Document pdfa="2b"> | No | No |
| Digital certification | PKCS#7 via certification prop or API | No | No |
| Redaction | True content removal + metadata scrubbing | No | No |
| PDF merging | Combine multiple PDFs | No | No |
| Rasterization | PDF → PNG via PDFium | No | No |
| Dependencies | None (WASM) | yoga-layout | Chrome/Chromium |
| Runs in-process | Yes | Yes | No (subprocess) |
See the templates/ directory for production-ready examples:
Style Forme components with Tailwind utility classes via @formepdf/tailwind:
npm install @formepdf/tailwind
import { tw } from '@formepdf/tailwind';
<View style={tw("flex-row items-center gap-4 p-6 bg-slate-100 rounded-lg")}>
<Text style={tw("text-2xl font-bold text-slate-900")}>Invoice</Text>
<Text style={tw("text-sm text-slate-500")}>Draft</Text>
</View>
Supports spacing, typography, colors (full Tailwind palette), flexbox, grid, borders, opacity, arbitrary values (w-[200], bg-[#f00]), negative values (-mt-4), fraction widths (w-1/2), and self-* alignment.
Full docs at docs.formepdf.com:
I'd like to hear about it: what you're generating, what you used before, and roughly how many documents a month. It directly shapes what gets built next. There's a short form at formepdf.com/production.
Issues and PRs welcome — see CONTRIBUTING.md for setup, the per-area test commands, and the house rules (fail-loud on unsupported CSS, keep the subset docs in sync, propose engine changes first). Security reports go through SECURITY.md.
MIT
1,409 followers · starred Apr 2026
261 followers · starred Sep 2026
23 followers · starred Mar 2026
117 followers · starred Apr 2026