> ## 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.

# HTML Input

> Render the HTML and print CSS you already have to paginated PDF — no headless browser. A documented subset with named warnings for everything outside it.

Forme 0.14 adds a second front door to the engine: **`@formepdf/html`** renders HTML + print CSS to PDF through the same Rust/WASM engine that renders JSX components. No headless browser, no Chromium cold start.

```bash theme={null}
# CLI — zero config
npx @formepdf/html invoice.html -o invoice.pdf

# with a separate stylesheet, page setup, and fonts
npx @formepdf/html report.html --css print.css --page-size Letter --margin 36 --font "Inter=Inter.ttf"
```

```ts theme={null}
// Library
import { renderHtml } from "@formepdf/html";

const { pdf, warnings } = await renderHtml(html, { css, fonts });
```

## The subset — the constitution

This is a deliberately **documented subset** of HTML/CSS, in the spirit of Satori — but page-native. Everything supported is listed below and tested. Everything outside the subset lands in the `warnings` list at render time, named, with a remedy where one exists. Nothing fails silently.

### Paged media — the point of the whole thing

| Supported | Not supported (warned) |
| - | - |
| `@page` `size` (named, dimensions, `landscape`) and `margin` | `@page` `bleed` / `marks` |
| `@page :first` — different margins, headers off the title page | |
| `@page :left` / `:right` — mirrored margins (left + right must sum equally with the base `@page`; unequal sums warn and normalize to the base), per-side margin boxes on edges the base `@page` also defines. Page 1 is a `:right` page (LTR page progression; `dir="rtl"` parity is not modeled — warned). `:first` outranks parity on page 1 | Per-side top/bottom margins (warned, normalized) |
| Named pages — `@page <name>` + `page: <name>`. The named run starts at a forced break, so top/bottom margins are real (a zero-margin cover works); left/right follow the mirrored-sum rule. Named margin boxes override/suppress the base running headers per name. Composes with `:first`/`:left`/`:right` (`@page cover:first`); named outranks `:first` outranks parity, per spec specificity | `:blank`, `:nth()` page groups, `string-set`/`content: string()`, `position: running()`, footnotes — warned by name |
| Margin boxes (`@top-center`, `@bottom-right`, …) for running headers/footers | |
| Page counters — `counter(page)` and `counter(pages)` | |
| `break-before` / `break-after` / `break-inside: avoid` (+ legacy `page-break-*` aliases) | |
| `orphans` / `widows` | |
| `@media` media-type evaluation — `print` is the native media type; feature queries evaluate against the page content box | `prefers-color-scheme`, `not` conditions (excluded with a named warning) |
| `<thead>` repetition across page breaks; table-cell overflow preservation | |

### Elements

| Supported | Not supported |
| - | - |
| Block containers: `div`, `section`, `article`, `header`, `footer`, `main`, `aside`, `nav`, `address`, `figure`, `blockquote`, `hr` | JavaScript of any kind (`<script>` skipped) |
| `h1`–`h6`, `p`, `br` | `<canvas>`, `<video>`, `<audio>`, `<iframe>`, forms |
| Inline: `span`, `b`/`strong`, `i`/`em`, `u`, `s`/`del`, `a`, `small`, `code`, `mark`, `sub`, `sup` | Inline `<img>` mid-paragraph |
| Tables: `table`, `thead`, `tbody`, `tfoot`, `tr`, `td`, `th` + `colspan`/`rowspan` | |
| Lists: `ul`, `ol` (+ `start`), `li` | |
| `img` (block-level) — data URIs and local files | External `http(s)` image fetching |
| `<style>` blocks; the CLI inlines **local** `<link rel="stylesheet">` | Remote stylesheets — never fetched, warned with the href and remedy |

### Selectors

| Supported | Not supported (selector skipped, warned) |
| - | - |
| Type, class, id, universal; compounds | Pseudo-elements (`::before`, `::after`) |
| Attribute selectors — all seven operators (`[checked]`, `[type=text]`, `[class~=well]`, `[lang\|=en]`, `[href^="https:"]`, `[src$=".png"]`, `[class*="span"]`) + the `i` case flag | Sibling combinators (`+`, `~`); namespaced attribute selectors |
| Descendant and child combinators; grouping | |
| `:first-child`, `:last-child`, `:nth-child(even\|odd\|an+b)` | `:only-child` and remaining tree pseudo-classes |
| `:first-of-type`, `:last-of-type`, `:nth-of-type`, `:nth-last-child`, `:nth-last-of-type` | Interaction pseudo-classes (`:hover`) — permanent: print has no hover |
| Full cascade with `!important` | |

### Properties

CSS Grid is supported as a **documented subset** (not all of grid): fr/px/auto/minmax tracks, integer `repeat()` — including Tailwind's `repeat(N, minmax(0, 1fr))` — gaps, spans, and numeric line placement; named lines, areas, `auto-flow`, `auto-fill/fit`, percentage tracks, and negative line numbers warn by name. The full property table lives in the [package README](https://github.com/formepdf/forme/blob/main/html/README.md) — margins with CSS margin collapsing, borders (solid/dashed/dotted with Chrome-matched dash metrics), `border-collapse` emulation, min/max width constraints (the centered-column idiom works), font fallback chains, `text-align: justify` with real Knuth-Plass distribution, `text-transform`, `letter-spacing`, `vertical-align` on cells, flexbox, relative/absolute positioning, floats as column rows (`float: left/right` + `clear` — consecutive floated siblings form a row, the Bootstrap `col-*` / left-right header-pair shape; text wrapping *alongside* a float is not supported and warns — following content renders below), CSS tables on divs (`display: table`/`table-row`/`table-cell` — the equal-height-columns idiom; a single-row one becomes a breakable column row), `position: fixed` as `position: absolute` by policy (anchored on the page where it occurs, not repeated per page — margin boxes are the running-content mechanism), and `body { overflow-x: hidden }` as a page-level clip (how off-viewport-parked sidebars stay invisible).

Notable exclusions, all warned by name: CSS variables, `position: sticky`, transforms, gradients, `@font-face` fetching (use `options.fonts` / `--font` — see the [web-fonts migration recipe](https://github.com/formepdf/forme/blob/main/html/README.md)).

## The warnings contract

```
$ npx @formepdf/html letterhead.html -o letterhead.pdf
warning: unsupported property: text-transform-origin
```

Every warning names the thing it skipped: the property, the selector, the stylesheet href, the `@font-face` family. If a render is silent, the document used only the subset — and what you got is what the subset defines.

## Browser, Workers, and edge

The HTML path ships web-target builds alongside Node (since 0.15.0), verified in real headless Chromium and real workerd:

```ts theme={null}
// Bundlers / browser
import { renderHtml } from "@formepdf/html/browser";

// Cloudflare Workers / edge — WASM must be initialized explicitly
import { init, renderHtml } from "@formepdf/html/worker";
import wasm from "@formepdf/html/pkg-web/forme_pdf_html_bg.wasm";

await init(wasm); // idempotent — warm isolates skip it
const { pdf, warnings } = renderHtml(html);
```

Sizing: the HTML engine WASM is 7.66 MB uncompressed, 3.41 MB gzipped (measured from the published 0.20.0 package; the CI-measured figure lives in the [parity artifact](https://parity.formepdf.com)). Size is no longer the Workers constraint — Cloudflare raised the limit to 64 MiB uncompressed on all plans (2026-09-04), so the module fits the free plan. The real constraint is CPU time: the free plan caps at 10 ms per request and a typical render is \~20 ms (measured in workerd, local Apple Silicon, 0.20.0), so real workloads need the paid plan. The module compiles and instantiates in \~1 ms in a Workers isolate, so its size is a disk/transfer cost, not a cold-start cost. Mind the Workers 128MB memory ceiling, though: table-heavy documents render comfortably up to \~50 pages and approach the limit near \~100 pages ([details](https://parity.formepdf.com/#benchmarks)).

## Migrating

* [Replacing Puppeteer](/replacing-puppeteer)
* [Migrating from wkhtmltopdf](/guides/migrating-from-wkhtmltopdf)
* [Migrating from DomPDF](/guides/migrating-from-dompdf)
