Documentation

Data & assets

Reactive data

The whole point of authoring in Vue is that your data drives the document. v-for, v-if, computed and slots all work - the renderer walks the same tree Vue builds, so anything that produces a tree produces a PDF.

<script setup lang="ts">
import { computed } from "vue";
import { Document, Page, Row, Text, Divider } from "@jasy/vue";

const props = defineProps<{ lines: { id: number; name: string; price: number }[] }>();
const total = computed(() => props.lines.reduce((sum, l) => sum + l.price, 0));
</script>

<template>
  <Document :size="11">
    <Page :size="'A4'" :margin="48" :gap="8">
      <Row v-for="line in lines" :key="line.id" :justify="'between'">
        <Text>{{ line.name }}</Text>
        <Text>{{ line.price }} EUR</Text>
      </Row>

      <Divider />
      <Row :justify="'between'">
        <Text bold>Total</Text>
        <Text bold>{{ total }} EUR</Text>
      </Row>
    </Page>
  </Document>
</template>

Custom fonts

The standard PDF fonts (Helvetica, Times, Courier) need nothing. To use your own, register the font bytes on <Document :fonts> under a name, then reference that name as a font:

<script setup lang="ts">
import { ref, onMounted } from "vue";

const font = ref<Uint8Array>();
onMounted(async () => {
  font.value = new Uint8Array(await (await fetch("/fonts/Inter.ttf")).arrayBuffer());
});
</script>

<template>
  <Document v-if="font" :fonts="{ Inter: font }" font="Inter">
    <Page :size="'A4'" :margin="48">
      <Text :size="24" bold>Set in Inter</Text>
    </Page>
  </Document>
</template>

The container does not matter: .ttf, .woff and .woff2 are the same thing here - jasy unwraps the wrapper itself. Full Unicode comes with it, and the font is subsetted on the way into the PDF, so the file stays small. For weights, register one file per style as { Inter: { normal, bold, italic } }.

With a bundler you do not have to fetch by hand - ask it for the URL and let jasy do the rest:

<script setup lang="ts">
import interUrl from "~/assets/Inter.woff2?url";

const doc = ref();
onMounted(async () => {
  // addFontFromUrl resolves AT registration, so a dead link fails here, not mid-render
  await doc.value.addFontFromUrl("Inter", interUrl);
});
</script>

On the server (Nuxt's definePdfHandler, or any Node process) a plain path works too - there :fonts="{ Inter: '/path/to/Inter.woff2' }" is the whole story. In the browser there is no filesystem to point at, so it is bytes or a URL.

Images

<Image :src> takes the image bytes as a Uint8Array. JPEG and PNG both work, and PNG transparency is honoured (it composites over whatever sits behind it, not a white box). Fetch the bytes the same way as a font:

<script setup lang="ts">
import { ref, onMounted } from "vue";

const logo = ref<Uint8Array>();
onMounted(async () => {
  logo.value = new Uint8Array(await (await fetch("/logo.png")).arrayBuffer());
});
</script>

<template>
  <Image v-if="logo" :src="logo" :width="120" :height="40" :fit="'contain'" />
</template>

In Node, read the file into a Uint8Array (for example with fs.readFileSync) and pass it the same way.

Render options

renderToPdf (and renderToPdfString) take a third argument for engine options. The most useful is onOverflow, which decides what happens when a single unbreakable element is taller than the page:

const bytes = await renderToPdf(Invoice, props, { onOverflow: "warn" });
  • "error" (default) - throw, so an impossible layout never slips out silently.
  • "warn" - place it anyway (clipped) and log.
  • "ignore" - place it anyway, no log.
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