A drag-and-drop PDF form designer that runs entirely in the browser. Build a document by dragging blocks onto a page, then export a real PDF with working AcroForm fields — the kind you can fill in with Acrobat, Preview, or any browser's PDF viewer.
Nothing is uploaded. There is no server, no API key and no account: the editor, the layout engine and the PDF writer all run on the client.
npm installnpm run devThen open http://localhost:5173. npm run build produces a static dist/ you
can host anywhere (or open from a file:// URL — base is relative).
Pushing to main deploys to GitHub Pages via
.github/workflows/deploy.yml. The custom domain
lives in public/CNAME — Vite copies public/ to the root of
dist/, so it rides along in the artifact and survives every redeploy.
Puck provides the drag-and-drop editing surface, and pdf-lib writes the PDF. In between sits a small layout engine of our own:
| Path | Role |
|---|---|
| src/puck/config.tsx | Block catalogue — the fields shown in the sidebar |
| src/puck/preview.tsx | How each block looks on the editing canvas |
| src/pdf/blocks.ts | How each block is drawn into the PDF |
| src/pdf/context.ts | Pages, cursor, fonts, page breaks, headers/footers |
| src/pdf/export.ts | Walks the document and hands back bytes |
The canvas preview is an approximation for designing against; the exporter is
the source of truth. Adding a block means adding it in three places: a renderer
in blocks.ts, a preview in preview.tsx, and an entry in config.tsx.
Content flows top-to-bottom with a cursor, exactly like a word processor.
Blocks ask for vertical space and the engine starts a new page when they do not
fit; paragraphs are the one block allowed to split mid-way. Columns are handled
with a measure pass — every renderer honours a dry flag that advances the
cursor without drawing, so a row can find its tallest column before committing
to a baseline.
Units are millimetres in the sidebar and points in the PDF, converted at the boundary in src/pdf/units.ts.
Content — heading, text, section header, divider, spacer, image, page break.
Form fields — text input (single or multi-line), date, dropdown, checkbox, checkbox group, radio group, signature. Each becomes a genuine AcroForm field.
Layout — 2- or 3-column rows with adjustable splits, and tables whose cells can optionally be fillable fields.
Document-wide settings (page size, orientation, margins, font, colours, running header and footer, field styling) live in the sidebar when nothing is selected.
Every form control carries a name, which is what appears when the filled PDF is read back programmatically. Leave Field name blank and it is derived from the label; collisions get a numeric suffix, since AcroForm names must be unique.
The document autosaves to localStorage as you work. Save template and
Open… move it in and out as JSON, which is the format to use for sharing a
design or keeping it in version control.
- Text uses the 14 standard PDF fonts, which are WinAnsi-encoded. Characters
outside that range (CJK, emoji) are replaced with
?rather than failing the export. Embedding a Unicode font would need@pdf-lib/fontkit. - Images must be PNG or JPEG, and are stored inline in the template, so large
ones can exhaust
localStorage. Export the template as JSON if autosave starts failing. - A column taller than the remaining page will overflow rather than break; keep rows short, or put a page break before them.
- "Flatten on export" produces a read-only PDF — the fields are painted onto the page and are no longer fillable.