Documentation
Pages
A Document holds one or more Pages. Each page sets its own size, orientation and margins, and when
its content is taller than the page, jasy flows it onto as many pages as it needs, on its own.
Page({ size: "A4", orientation: "portrait", margin: 56 }, [Text("One page.")]);
| Prop | Type | What it does |
|---|---|---|
size | PageSize or a name like "A4", "letter" | the page format (default A4) |
orientation | "portrait" | "landscape" | rotates the format (default portrait) |
margin | Insets | the content box inset (default 56pt all sides) |
header | an element | drawn at the top of every page |
footer | an element | drawn at the bottom of every page |
A Page also takes the Column options (gap, justify, align), since its
children are stacked in a column.
Many pages
Hand Document a list of pages. Each can differ, mixing sizes and orientations freely.
Document([
Page({ size: "A4" }, [Text("Cover")]),
Page({ size: "A5", orientation: "landscape" }, [Text("A landscape insert")]),
]);
Headers, footers and pagination
A header and footer are laid out once and reprinted on every page the content spills onto, so a long
table or a long stack of paragraphs keeps its letterhead and page furniture throughout. You write the
content once; jasy paginates.
import { writeFileSync } from "node:fs";
import { Document, Page, Column, Row, Text, renderToBytes } from "@jasy/pdf";
const header = Row({ justify: "between" }, [
Text("Acme Inc.", { bold: true, color: "#1450aa" }),
Text("Report"),
]);
const footer = Row({ justify: "between" }, [
Text("Confidential", { size: 9, color: "gray" }),
Text("2026", { size: 9, color: "gray" }),
]);
async function build() {
const body = Array.from({ length: 60 }, (_, i) =>
Text(`Paragraph ${i + 1} - long body text that flows across pages.`, { size: 12 }),
);
const doc = Document([
Page({ size: "A4", margin: 56, header, footer }, [Column({ gap: 6 }, body)]),
]);
writeFileSync("report.pdf", await renderToBytes(doc));
}
build();
The 60 paragraphs are taller than one page, so the result is several pages, each carrying the same header and footer.
Page breaks
jasy paginates on its own, but sometimes you want to decide where a break falls - start a section on a
fresh page, or stop a block from being cut in half at the boundary. These mirror the CSS
break-before / break-after / break-inside properties.
Force a new page
PageBreak() sends everything after it to a fresh page, no matter how much room is left.
import { Document, Page, Column, Text, PageBreak } from "@jasy/pdf";
Column([Text("Chapter one ends here."), PageBreak(), Text("Chapter two starts on a new page.")]);
Break before or after an element
The same thing as a prop - put breakBefore or breakAfter on any Box, Column or Row
(CSS break-before / break-after: page):
Column([
Text("Summary"),
Box({ breakBefore: true, padding: 12 }, [Text("Appendix - always starts its own page")]),
]);
A breakBefore at the very top of a page is ignored, so you never get a stray blank page.
Keep a block together
keepTogether refuses to split a group across a page boundary (CSS break-inside: avoid). If the group
does not fit in the space left but would fit on a fresh page, it moves there whole; if it is taller than
a whole page, it splits anyway, so pagination always finishes. Nesting works - an inner keepTogether
still holds even when an outer one has to give.
import { keepTogether } from "@jasy/pdf";
keepTogether([
Text("Invoice total", { bold: true }),
Divider(),
Text("Subtotal 100.00"),
Text("Tax 19.00"),
Text("Total 119.00", { bold: true }),
]);
// or as a prop: Box({ keepTogether: true, padding: 12 }, [ /* ... */ ])
All four are available in @jasy/vue and @jasy/nuxt as well: <PageBreak />, <KeepTogether>, and
the break-before / break-after / keep-together props.
Page numbers
PageNumber() and PageCount() print the current page and the document total. Drop them wherever you
like - a footer is the usual home, but nothing stops you putting them in the body, in a table cell, or
five times on one page.
Page(
{ footer: Row({ justify: "center" }, [Text("Page "), PageNumber(), Text(" of "), PageCount()]) },
[Column(body)],
);
Both take the usual Text options plus an offset, which is added to the number. Use
it when a cover page should not count:
PageNumber({ offset: -1, size: 9, color: "gray" });
Building your own
Behind both sits one primitive: PageBuilder. It hands you the page's facts and draws whatever you
return, so formatting and conditions are plain JavaScript.
import { Document, Page, Column, Row, Text, PageBuilder } from "@jasy/pdf";
const doc = Document([
Page(
{
// A full title on page 1, a quiet continuation line afterwards.
header: PageBuilder(({ pageNumber }) =>
pageNumber === 1
? Text("Acme Inc. - Annual Report", { size: 16, bold: true })
: Text("Annual Report (continued)", { size: 9, color: "gray" }),
),
footer: PageBuilder(({ pageNumber, pageCount }) =>
Text(`Page ${pageNumber} of ${pageCount}`, { size: 9, align: "center" }),
),
},
[Column(body)],
),
]);
The closure receives pageNumber (1-based, counting physical pages, so a Page that overflows
contributes several), pageCount (the document total) and pageSize (the media box in points).
Getting pageCount right is why jasy paginates the whole document before it draws anything: the total
has to exist before page 1 is painted. That ordering leaves two things worth knowing, and they follow
from the same chicken-and-egg - the content decides the count that the content displays.
A conditional header may shrink on later pages, never grow. The body band is measured against the first build, so a taller header on page 3 would squeeze the body it was already sized for. Shrinking only frees up room, which is harmless.
Inside the flowing body the box is reserved before the total is known, so a much wider final string
("9 of 10" where "1 of 1" was measured) can paint slightly past it. Keep dynamic body content roughly
constant in width. In a header or footer the numbers are always exact.