How I built Word-style A4 pages for TipTap without touching the document
Description and use
A free npm package tiptap-a4-pagination adds Word-style A4 pagination to TipTap editors without modifying the document or user CSS, with 13 unit tests for the core layout function and browser-based PDF export.
Problem
Users of a TipTap/ProseMirror editor want Word-like A4 pagination and exact print fidelity, but existing community extensions inject global CSS that breaks existing styles, and the official Tiptap Pages extension is paid and private.
Outcome
A free npm package tiptap-a4-pagination adds Word-style A4 pagination to TipTap editors without modifying the document or user CSS, with 13 unit tests for the core layout function and browser-based PDF export.
Workflow
- Measure every top-level block (paragraphs, headings, tables, images).
- When a block would cross the bottom of a page, insert a widget decoration spacer right before it.
- Make the widget exactly as tall as the rest of the page plus bottom margin plus the gray gap plus the next page's top margin.
- Rebuild the layout from heights and margins only, ignoring any previously inserted spacers, to avoid measurement loops.
- Handle edge cases: margin collapse, floated images, table row splitting, rowspan grouping, repeated header rows, and zoom scaling.
- Export PDF by printing through the browser using window.print() on a hidden iframe, with @page rules and CSS page margin boxes.
Evidence
If you've built an editor with TipTap / ProseMirror, you've probably heard this request: Make it look like Word. Show real A4 pages, and print exactly what I see.
I wanted one thing: don't modify the document, don't touch the user's CSS. editor.getHTML() should stay exactly as clean as before.
ProseMirror has decorations: elements that are rendered but are not part of the document.
Measure every top-level block (paragraphs, headings, tables, images…). When a block would cross the bottom of a page, insert a widget decoration right before it. Make the widget exactly as tall as the rest of the page + bottom margin + the gray gap + the next page's top margin.
The document doesn't change by a single byte.
My first attempt used each block's getBoundingClientRect().top. The problem: the spacers I had already inserted shift those positions. Measure again, get a different answer, move the spacers, measure again… an endless loop.
What works is to rebuild the layout from heights and margins only, as if there were no spacers at all.
This function is pure: blocks in, page breaks out, no DOM. That made it easy to unit test, and the package has 13
- Record date: 2026-10-23T09:11:17.460Z