Documentation

Components

Every component maps to one engine element, and its props are typed - <Text :size :color bold> autocompletes and type-checks, and a wrong value (say a fit that does not exist) goes red in your editor. Boolean props have a shorthand: <Text bold> is <Text :bold="true">.

The full set:

Document · Page · Column · Row · Box · Padding · Expanded · Spacer · Divider · Image · Text · Paragraph · Span · Table / TableRow / TableCell · Positioned · DefaultTextStyle · Link · Anchor · Bookmark · Rotated · RotatedBox · PageNumber · PageCount.

Structure

Document

The root of every PDF component. Its text props (font, size, color, lineHeight, align, bold, italic) set the document-wide defaults that everything inside inherits.

PropType
size, color, font, bold, italicdefault text style
align, lineHeightdefault paragraph style
meta{ title?, author? }
fontscustom fonts, see Data & assets

Page

One physical page. 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"

<Page> also takes #header and #footer slots. They are drawn on every physical page the content flows onto, and rebuilt per page - which is how <PageNumber> knows where it is:

<template>
  <Document>
    <Page :size="'A4'" :margin="48">
      <template #header>
        <Text :size="9" color="#94a3b8">ACME GmbH - Invoice</Text>
      </template>

      <Text>... long content that paginates across pages ...</Text>

      <template #footer>
        <Row :justify="'between'">
          <Text :size="9" color="#94a3b8">acme.example</Text>
          <Text :size="9" color="#94a3b8">Page footer</Text>
        </Row>
      </template>
    </Page>
  </Document>
</template>

Layout

Column and Row

Stack children vertically (Column) or horizontally (Row). Both share gap, justify and align.

<template>
  <Row :gap="12" :justify="'between'" :align="'center'">
    <Text bold>Item</Text>
    <Text>Value</Text>
  </Row>
</template>

Box

A sized, decorated container: background, border, padding, rounded corners.

PropType
bg, bordera color
borderTop / borderRight / borderBottom / borderLeft, borderWidthper-side border control
paddinga number, or per-side insets
width, height, radiusnumbers (points)
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"> - a flex child that fills the remaining main-axis space.
  • <Divider color="#e2e8f0" :thickness="1" /> - a rule line.

Text

Text, Paragraph and Span

Text and Paragraph render text; Span styles a run inside them.

PropType
size, font, colorthe basics
bold, italicboolean shorthands
align"left" | "center" | "right"
lineHeightline spacing multiplier
maxLines, overflowoverflow is "clip" | "ellipsis"
hreflink this run to an external URL
tolink this run to an <Anchor>
<template>
  <Paragraph>
    Plain text, then a <Span bold color="#1450aa">styled run</Span> in the middle.
  </Paragraph>
</template>

bold and italic are left unset by default, so a <Text> inside a bold <DefaultTextStyle> stays bold unless you override it. <DefaultTextStyle> re-defaults the text style for a whole subtree, the per-section counterpart to the <Document> defaults.

Putting href (or to) on a <Span> links just that run and lets the sentence flow around it:

<template>
  <Paragraph>
    Built with <Span href="https://jasy.dev" bold color="#1450aa">jasy</Span>, and this part is
    not a link.
  </Paragraph>
</template>

Image

<template>
  <Image :src="bytes" :width="120" :height="120" :fit="'contain'" :radius="8" />
</template>

fit is "none", "contain", "cover" or "fill". src takes image bytes (a Uint8Array); see Data & assets for loading them in the browser.

Table

<Table :columns> holds <TableRow>s of <TableCell>s. Mark a row header to repeat it on every page the table paginates onto. Columns are widths: a number is fixed points, a string like "1fr" is a flexible share.

<template>
  <Table :columns="['1fr', 120]" cell-border="#e2e8f0" :cell-padding="9">
    <TableRow header>
      <TableCell><Text bold>Description</Text></TableCell>
      <TableCell><Text bold>Amount</Text></TableCell>
    </TableRow>
    <TableRow v-for="line in lines" :key="line.id">
      <TableCell
        ><Text>{{ line.name }}</Text></TableCell
      >
      <TableCell
        ><Text>{{ line.total }} EUR</Text></TableCell
      >
    </TableRow>
  </Table>
</template>

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.

<template>
  <Box relative :height="200">
    <Positioned :top="8" :right="8">
      <Text :size="9" color="#94a3b8">draft</Text>
    </Positioned>
  </Box>
</template>

<Link> makes its child clickable. href opens a URL, to jumps to an <Anchor> in the same document. Exactly one of the two.

<template>
  <Link href="https://jasy.dev">
    <Box bg="#f3dc29" :padding="12"><Text>The whole box is the hit area</Text></Box>
  </Link>
</template>

<Anchor name> marks a jump target, and <Bookmark title> adds an entry to the outline tree your viewer shows in its sidebar. level nests it under the nearest preceding smaller level.

ComponentProps
<Link>href | to
<Anchor>name (required)
<Bookmark>title (required), level

A chapter heading usually wants both: a sidebar entry and a jump target. Nest them.

<template>
  <Bookmark title="2. Installation" :level="1">
    <Anchor name="install">
      <Text :size="22" bold>2. Installation</Text>
    </Anchor>
  </Bookmark>
</template>

See Navigation for how links, anchors and bookmarks work under the hood.

Rotated and RotatedBox

<Rotated :angle> spins its child at any angle around its center, at paint time: the layout box stays unrotated, so siblings do not reflow. That is what you want for a stamp or a watermark.

<RotatedBox :turns> does layout-aware quarter-turns instead. A 90 or 270 degree turn swaps width and height, so a vertical label reserves exactly the strip it occupies and its neighbours flow around it. turns counts clockwise 90-degree steps.

<template>
  <Row :gap="12">
    <RotatedBox :turns="3"><Text bold>SECTION A</Text></RotatedBox>
    <Column><Text>Line one</Text><Text>Line two</Text></Column>
  </Row>

  <Rotated :angle="-18">
    <Box bg="#ffe9e9" border="#d33" :padding="10"><Text :size="26" bold color="#d33">PAID</Text></Box>
  </Rotated>
</template>

Page numbers

<PageNumber> and <PageCount> print the current page and the document total. Put them anywhere - a #footer is the usual home, but the body works too. They take the <Text> styling props plus offset, which is added to the number (use -1 when a cover page should not count).

<template>
  <Document>
    <Page>
      <template #footer>
        <Row justify="center" :gap="3">
          <Text :size="9">Page</Text>
          <PageNumber :size="9" bold />
          <Text :size="9">of</Text>
          <PageCount :size="9" bold />
        </Row>
      </template>
      <Column><!-- body --></Column>
    </Page>
  </Document>
</template>

Getting the total right is why jasy paginates the whole document before it draws anything.

The core also ships a PageBuilder primitive, which hands you pageNumber, pageCount and pageSize and draws whatever you return. It takes a closure, so it is not exposed as a component - a template cannot express one. In a Nuxt server/ route, where you write plain JavaScript, you can use it directly.

jasypdf

Declarative PDFs in pure TypeScript. ZUGFeRD & XRechnung compliant, with no headless browser and no Java.

Resources

© 2026 Florian Heuberger · MIT License

Built with Nuxt · self-hosted fonts · no trackers