Documentation
Creating PDFs
PDFs in Vue
PDFs in Nuxt
PDFs in React
PDFs in Angular
Components
Every component maps to one engine element, and every prop type IS the engine's option type: a new
engine option is a typed prop the day it ships, and a wrong value goes red in your editor. Boolean props
have the usual JSX shorthand: <Text bold> is <Text bold={true}>.
The full set:
Document · Page · Column · Row · Box · Padding · Expanded · Spacer · Divider ·
Image · Svg · Canvas · Text · Paragraph · Span · Table / TableRow / TableCell ·
Positioned · DefaultTextStyle · Link · Anchor · Bookmark · Rotated · RotatedBox ·
PageBreak · KeepTogether · PageNumber · PageCount · SubPageNumber · SubPageCount ·
PageBuilder · SplitView · Flow · Region · ColumnBreak · TextField · Checkbox ·
RadioGroup · Dropdown · ListBox · PushButton · SignatureField.
Where React has its own way, it is used: text and labels are children, repeating areas are props, and what is built per page is a function.
Structure
Document
The root of every PDF. Its text props set the document-wide defaults that everything inside inherits.
| Prop | Type |
|---|---|
size, color, font, bold, italic | default text style - any inheritable text style |
align, lineHeight | default paragraph style |
meta | { title?, author? } |
fonts | custom fonts, see Data & assets |
emoji | a color-emoji source, see Emoji |
Page
One page template. It lays its children out as a column, so gap, justify and align work here too.
| Prop | Type |
|---|---|
size | "A4", "Letter", ... or a custom size |
orientation | "portrait" | "landscape" |
margin | a number, or per-side insets |
gap | space between children |
justify | "start" | "center" | "end" | "between" | "around" |
align | "start" | "center" | "end" | "stretch" |
header, footer | any element, repeated on every page |
header and footer are laid out once and repeated on every physical page the content flows onto,
with the space they need reserved - the content never runs under them:
<Document>
<Page
size="A4"
margin={48}
header={
<Text size={9} color="#94a3b8">
ACME GmbH - Invoice
</Text>
}
footer={
<Row justify="between">
<Text size={9} color="#94a3b8">
acme.example
</Text>
<PageNumber size={9} color="#94a3b8" />
</Row>
}
>
<Text>... long content that paginates across pages ...</Text>
</Page>
</Document>
Layout
Column and Row
Stack children vertically (Column) or horizontally (Row). Both take the options of the engine's
stacks: gap, justify, align, wrap, reverse, alignContent, a width and height in points
or "%", and the page-break props below.
<Row gap={12} justify="between" align="center">
<Text bold>Item</Text>
<Text>Value</Text>
</Row>
Every child can leave its line's alignment with alignSelf, move with order and take minWidth,
maxWidth, minHeight, maxHeight and aspectRatio - see Layout.
Box
A sized, decorated container: background, border, padding, rounded corners.
| Prop | Type |
|---|---|
bg | a color, or a gradient |
border | a color |
borderTop / borderRight / borderBottom / borderLeft, borderWidth | per-side border control |
padding | a number, or per-side insets |
width, height | points, or a percentage like "50%" |
radius | a number, or per-corner radii |
relative | make it a positioning frame |
overflow | "hidden" clips children to the box |
Padding, Spacer, Expanded, Divider
<Padding insets={16}>- wraps a child in space (a number or per-side insets).<Spacer flex={1} />- flexible empty space that pushes siblings apart.<Expanded flex={1}>- makes its child a flex item that takes its share of the remaining space;flexBasisandflexShrinkas in CSS.<Divider color="#e2e8f0" thickness={1} />- a rule line.
Text
Text, Paragraph and Span
Text and Paragraph render their children as text; Span styles a run inside them. Total: {sum} is
one run, measured and kerned as one.
| Prop | Type |
|---|---|
size, font, color | the basics; font also takes a fallback stack |
bold, italic | boolean shorthands |
align | "left" | "center" | "right" | "justify" |
lineHeight | line spacing multiplier |
letterSpacing, wordSpacing | in points; negative tightens |
textIndent | first-line indent, in points |
textTransform | "uppercase" | "lowercase" | "capitalize" | "none" |
underline, strikethrough, skipInk | decoration; skipInk steps around descenders |
direction | "ltr" | "rtl" - Hebrew and Arabic order correctly |
ligatures | false turns off fi/fl/ffi, which are on by default |
breakWord, hyphenate | what to do with a word too long for its box |
orphans, widows | minimum lines kept together at a page break |
maxLines, overflow | overflow is "clip" | "ellipsis" |
role | "h1".."h6" | "p" for a tagged, accessible PDF |
href | link the text to an external URL |
to | link the text to an <Anchor> |
<Span> takes the run styles - size, font, color, bold, italic, the decorations,
letterSpacing, wordSpacing, textTransform, ligatures, breakWord, hyphenate - plus href /
to and verticalAlign ("super" | "sub"), which shifts one run's baseline without resizing it.
What belongs to the whole paragraph stays on <Text>: align, lineHeight, textIndent,
direction, maxLines, overflow, orphans, widows and role.
<Paragraph>
Built with{" "}
<Span href="https://jasy.dev" bold color="#1450aa">
jasy
</Span>
, and this part is not a link.
</Paragraph>
Whitespace between spans is yours to write, as {" "} - JSX drops the line breaks around a tag.
<DefaultTextStyle> re-defaults the text style for a whole subtree, the per-section counterpart to the
<Document> defaults.
Image
<Image src={bytes} width={120} height={120} fit="cover" align="top" radius={8} alt="Logo" />
src takes image bytes (a Uint8Array), a URL, or - on the server - a path; see
Data & assets. fit is "fill" (the default), "contain", "cover" or
"none", and align says where the image sits in its box. Without a size the image is its own size,
scaled down to fit. The props are those of the Image factory. An SVG source is
recognized by itself and stays a vector.
Svg
<Svg src={logoMarkup} width={120} alt="Acme" />
src takes SVG markup, the file's bytes, or - on the server - a path. With no size it draws at its
intrinsic size; pin one axis and the other follows the viewBox. alt makes it a Figure in a tagged
PDF. See SVG and Canvas for what the subset covers.
Canvas
For a chart, a sparkline or a seal, <Canvas> hands your function a typed pen and the size the layout
resolved:
import type { CanvasPaint } from "@jasy/pdf";
const months = [12, 19, 14, 27, 22, 31];
const sparkline: CanvasPaint = (c, { width, height }) => {
const max = Math.max(...months);
c.move(0, height - (months[0] / max) * height);
months
.slice(1)
.forEach((v, i) =>
c.line(((i + 1) / (months.length - 1)) * width, height - (v / max) * height),
);
c.stroke("#1450aa", { width: 1.5, cap: "round" });
};
<Canvas height={46} paint={sparkline} />;
The same function works at any width, so one definition serves a wide card and a narrow sidebar.
Table
<Table columns> holds <TableRow>s of <TableCell>s. Mark a row header to repeat it on every page
the table flows onto. A number is a fixed width in points, "1fr" a flexible share, "auto" the widest
cell.
<Table columns={["1fr", 120]} cellBorder="#e2e8f0" cellPadding={9}>
<TableRow header>
<TableCell>
<Text bold>Description</Text>
</TableCell>
<TableCell>
<Text bold>Amount</Text>
</TableCell>
</TableRow>
{lines.map((line) => (
<TableRow key={line.id}>
<TableCell>{line.name}</TableCell>
<TableCell>{`${line.total} EUR`}</TableCell>
</TableRow>
))}
</Table>
A cell holds a string or any components. Table also takes gap / rowGap / colGap, cellPadding,
cellBorder and a rule color.
Positioned
Out-of-flow placement, anchored to the nearest <Box relative> (or the page). Give it edges (top /
right / bottom / left) or an alignment (h / v) plus an x / y offset. zIndex sets the
paint order - see Positioning.
<Box relative height={200}>
<Positioned top={8} right={8}>
<Text size={9} color="#94a3b8">
draft
</Text>
</Positioned>
</Box>
Navigation
<Link> makes its child clickable. href opens a URL, to jumps to an <Anchor> in the same
document - exactly one of the two, and the types say so.
| Component | Props |
|---|---|
<Link> | href | to |
<Anchor> | name (required) |
<Bookmark> | title (required), level |
A chapter heading usually wants a sidebar entry and a jump target. Nest them:
<Bookmark title="2. Installation" level={1}>
<Anchor name="install">
<Text size={22} bold>
2. Installation
</Text>
</Anchor>
</Bookmark>
See Navigation for how links, anchors and bookmarks work under the hood.
Rotated and RotatedBox
<Rotated angle> spins its child at paint time: the layout box stays unrotated, so siblings do not
reflow - a stamp or a watermark. <RotatedBox turns> does layout-aware quarter-turns: a 90 or 270
degree turn swaps width and height, so a vertical label reserves exactly the strip it occupies.
<Row gap={12}>
<RotatedBox turns={3}>
<Text bold>SECTION A</Text>
</RotatedBox>
<Rotated angle={-18}>
<Box bg="#ffe9e9" border="#d33" padding={10}>
<Text size={26} bold color="#d33">
PAID
</Text>
</Box>
</Rotated>
</Row>
Page breaks
<PageBreak /> cuts the flow: everything after it starts on a fresh page. <KeepTogether> refuses to
let its children be split and moves the whole group to the next page if it would not fit. Both also
exist as props (breakBefore, breakAfter, keepTogether) on <Box>, <Column> and <Row>.
<Page>
<Text size={18} bold>
Terms
</Text>
<PageBreak />
<Box keepTogether border="#d0d4da" radius={8} padding={14}>
<Text bold>Signed</Text>
<Text size={9}>This box is never torn across two pages.</Text>
</Box>
</Page>
Page numbers
<PageNumber> and <PageCount> print the current page and the document total; <SubPageNumber> and
<SubPageCount> count within one <Page>, for a document made of sections. They take the <Text>
props plus offset, which is added to the number (-1 when a cover page should not count).
For anything else that depends on the page, <PageBuilder> takes a function as its child and builds
it for every page it lands on. Return null to show nothing on a page:
<Page
header={
<PageBuilder>
{({ pageNumber }) =>
pageNumber === 1 ? null : <Text size={9}>Annual Report, continued</Text>
}
</PageBuilder>
}
footer={
<PageBuilder>
{({ pageNumber, pageCount }) => (
<Text size={9} align="center">
Page {pageNumber} of {pageCount}
</Text>
)}
</PageBuilder>
}
>
{/* body */}
</Page>
The function receives pageNumber, pageCount, pageSize, subPageNumber and subPageTotalPages.
What it builds is placed whole on its page. Getting the total right is why jasy paginates the whole
document before it draws anything.
Columns
<SplitView> sets its content in newspaper columns that continue on the next page and balance on the
last one. <ColumnBreak /> starts the next column.
<Page gap={12}>
<Text size={28} bold color="#1450aa">
The Valley Courier
</Text>
<SplitView columns={3} colGap={16} rowGap={8}>
{paragraphs.map((p) => (
<Text key={p.id}>{p.text}</Text>
))}
</SplitView>
<Text italic>This line follows the columns, right where they end.</Text>
</Page>
For a layout of your own, <Flow> takes it as the page render prop: one page of the layout, with a
<Region /> wherever the content goes. It receives { index, pageNumber }, so the first page can look
different from the rest.
<Flow
gap={8}
page={({ index }) => (
<Column gap={12}>
{index === 0 ? (
<Text size={22} bold color="#1450aa">
Annual Report
</Text>
) : (
<Text size={9} italic>
Annual Report, continued
</Text>
)}
<Row gap={16}>
<Expanded>
<Region />
</Expanded>
<Box bg="#f3f5f9" padding={8}>
<Region width={150} />
</Box>
</Row>
</Column>
)}
>
{report.map((p) => (
<Text key={p.id}>{p.text}</Text>
))}
</Flow>
page and a PageBuilder's function are rendered in place, in your tree, while the PDF is laid out -
hooks and context work in them as anywhere else. The options, and the errors that name a mistake, are on
the Columns page.
Form fields
A form field is a layout element like any other: it reserves a box, flows with its neighbors, and emits a widget the reader can type into. Seven kinds:
| Component | For |
|---|---|
TextField | one line or, with multiline, several; password, maxLength |
Checkbox | a single tick; onValue is what gets stored |
RadioGroup | one of several, laid out for you; options + value |
Dropdown | a closed list, or editable to allow free text |
ListBox | a visible list; multiSelect for several at once |
PushButton | an action - "reset", { open: url } or { submit: url } |
SignatureField | an empty signature box for the reader to sign |
Every field takes name (the key the value is stored under), required, hidden, print and the
appearance props border, background and color. The single widgets add tooltip, readOnly and
borderWidth; the boxed ones (TextField, Dropdown, ListBox, PushButton, SignatureField) take
width and height, a Checkbox takes size. A RadioGroup is several buttons, so it has none of
those three: it takes size, gap and the label styling of its choices. The label of a Checkbox, a
PushButton or a SignatureField is its children:
<Column gap={10}>
<TextField name="fullName" width={220} height={22} border="#aab3c2" background="#fbfcfe" />
<RadioGroup
name="plan"
gap={6}
options={[
{ value: "basic", label: "Basic" },
{ value: "pro", label: "Pro" },
]}
/>
<Dropdown name="country" width={110} height={22} options={["DE", "AT", "CH"]} />
<Checkbox name="agree">I have read the statutes</Checkbox>
<PushButton name="clear" action="reset" width={92} height={26}>
Reset form
</PushButton>
</Column>
There is no onChange: a generated PDF writes nothing back, so value is what the field starts with.
Reading a filled form back lives in the core: Forms.
A component of your own
Your own components are React components - compose them from the set above, with props, hooks and context:
function Callout({ children }: { children: string }) {
return (
<Box bg="#eef1f5" border="#1450aa" radius={6} padding={12}>
<Text color="#1450aa">{children}</Text>
</Box>
);
}