Documentation

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.

PropType
size, color, font, bold, italicdefault text style - any inheritable text style
align, lineHeightdefault paragraph style
meta{ title?, author? }
fontscustom fonts, see Data & assets
emojia 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.

PropType
size"A4", "Letter", ... or a custom size
orientation"portrait" | "landscape"
margina number, or per-side insets
gapspace between children
justify"start" | "center" | "end" | "between" | "around"
align"start" | "center" | "end" | "stretch"
header, footerany 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.

PropType
bga color, or a gradient
bordera color
borderTop / borderRight / borderBottom / borderLeft, borderWidthper-side border control
paddinga number, or per-side insets
width, heightpoints, or a percentage like "50%"
radiusa number, or per-corner radii
relativemake 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; flexBasis and flexShrink as 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.

PropType
size, font, colorthe basics; font also takes a fallback stack
bold, italicboolean shorthands
align"left" | "center" | "right" | "justify"
lineHeightline spacing multiplier
letterSpacing, wordSpacingin points; negative tightens
textIndentfirst-line indent, in points
textTransform"uppercase" | "lowercase" | "capitalize" | "none"
underline, strikethrough, skipInkdecoration; skipInk steps around descenders
direction"ltr" | "rtl" - Hebrew and Arabic order correctly
ligaturesfalse turns off fi/fl/ffi, which are on by default
breakWord, hyphenatewhat to do with a word too long for its box
orphans, widowsminimum lines kept together at a page break
maxLines, overflowoverflow is "clip" | "ellipsis"
role"h1".."h6" | "p" for a tagged, accessible PDF
hreflink the text to an external URL
tolink 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>

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

ComponentProps
<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:

ComponentFor
TextFieldone line or, with multiline, several; password, maxLength
Checkboxa single tick; onValue is what gets stored
RadioGroupone of several, laid out for you; options + value
Dropdowna closed list, or editable to allow free text
ListBoxa visible list; multiSelect for several at once
PushButtonan action - "reset", { open: url } or { submit: url }
SignatureFieldan 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>
  );
}