Core Concepts

Advanced

Everything beyond laying out a static page: controlling how content flows across pages, making documents navigable, generating content that depends on the page it lands on, and keeping large renders off the main thread.

Page Wrapping

vue-pdf ships a wrapping engine that is enabled by default. When content exceeds the page, it breaks onto a new one automatically. Turn it off per page with :wrap="false":

vue
<template>
  <Document>
    <!-- Disable the wrapping engine for this page -->
    <Page :wrap="false">
      <Text>Everything stays on one page, even if it overflows.</Text>
    </Page>
  </Document>
</template>

Breakable and Unbreakable Components

<View>, <Text>, and <Link> are breakable — they fill the remaining space and continue on the next page. <Image> is unbreakable — if it does not fit, the whole thing moves to the next page.

Set :wrap="false" on a breakable element to make it behave the same way:

vue
<template>
  <Document>
    <Page wrap>
      <!-- Pushed whole onto the next page if it doesn't fit here -->
      <View :wrap="false" :style="{ padding: 12 }">
        <Text>This block is never split across pages.</Text>
      </View>
    </Page>
  </Document>
</template>

Page Breaks

The break prop on any primitive forces a new page before it renders:

vue
<template>
  <Document>
    <Page wrap>
      <Text>End of section one.</Text>

      <!-- Forces a new page before rendering -->
      <Text break>Section two starts on a fresh page.</Text>
    </Page>
  </Document>
</template>

Fixed Components

The fixed prop repeats an element on every page — the basis for headers, footers, and page numbers:

vue
<template>
  <Document>
    <Page wrap :style="{ paddingTop: 50, paddingBottom: 40 }">
      <!-- Repeated on every page -->
      <View
        fixed
        :style="{ position: 'absolute', top: 16, left: 40, right: 40 }"
      >
        <Text :style="{ fontSize: 9, color: '#888' }">Quarterly Report</Text>
      </View>

      <Text
        fixed
        :style="{ position: 'absolute', bottom: 16, left: 0, right: 0, textAlign: 'center', fontSize: 9 }"
        :render="({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`"
      />

      <Text>Body content...</Text>
    </Page>
  </Document>
</template>

Document Navigation

Named Destinations

Give an element an id, then point a <Link> at it with a # prefix. The reader jumps to that element, across pages:

vue
<template>
  <Document>
    <Page>
      <Link href="#footnote">Jump to the footnote</Link>

      <View id="footnote" break>
        <Text>You are here because you clicked the link above.</Text>
      </View>
    </Page>
  </Document>
</template>

Bookmarks

The bookmark prop builds the outline tree readers show in their sidebar. It accepts either a string or a bookmark object, and nesting follows your component tree — a bookmark on a <Text> inside a bookmarked <Page> becomes its child:

vue
<template>
  <Document>
    <Page bookmark="Harry Potter and the Philosopher's Stone">
      <Text :bookmark="{ title: 'Chapter 1: The Boy Who Lived', fit: true }">
        Mr and Mrs Dursley, of number four, Privet Drive...
      </Text>

      <Text
        break
        :bookmark="{ title: 'Chapter 2: The Vanishing Glass', expanded: true }"
      >
        Nearly ten years had passed...
      </Text>
    </Page>
  </Document>
</template>
PropTypeDefaultDescription
title*stringThe label shown in the reader's outline.
topnumber0Y coordinate the reader scrolls to.
leftnumber0X coordinate the reader scrolls to.
zoomnumberZoom level applied when the bookmark is followed.
fitbooleanJump to the start of the page rather than a coordinate.
expandedbooleanShow this node already expanded in the outline tree.
Some older PDF readers ignore bookmarks. Treat the outline as navigation convenience, not as the only way to reach a section.

Dynamic Content

Pass a function to the render prop of <Text> or <View> to generate content that depends on where it lands:

vue
<template>
  <Document>
    <Page wrap>
      <!-- Page numbers in a footer -->
      <Text
        fixed
        :render="({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`"
      />

      <!-- Conditional content per page -->
      <View :render="({ pageNumber }) => renderOddPageBanner(pageNumber)" />
    </Page>
  </Document>
</template>

A <Text> render function returns a string. A <View> render function returns plain element objects:

vue
<script setup lang="ts">
// A View render function returns plain element objects — NOT Vue vnodes.
// `type` is the primitive's string name, and children nest the same way.
const renderOddPageBanner = (pageNumber: number) =>
  pageNumber % 2 === 1
    ? {
        type: 'VIEW',
        props: {},
        style: { backgroundColor: '#fee', padding: 8 },
        children: [
          {
            type: 'TEXT',
            props: {},
            style: { fontSize: 10 },
            children: "I'm only visible on odd pages!",
          },
        ],
      }
    : null
</script>
A <View> render function must not return Vue vnodes. Anything built with h() has a component object as its type and is silently dropped — no error, just missing content. Return objects whose type is the primitive's string name instead.

To avoid hardcoding those strings, import the primitive constants:

vue
<script setup lang="ts">
import * as P from '@vuepdf/renderer/primitives'

// P.View === 'VIEW', P.Text === 'TEXT' — the same strings, but checked
const renderBanner = (pageNumber: number) => ({
  type: P.View,
  props: {},
  style: { backgroundColor: '#fee', padding: 8 },
  children: [
    { type: P.Text, props: {}, children: `Page ${pageNumber}` },
  ],
})
</script>
PropTypeDefaultDescription
pageNumbernumberThe current page number.
totalPagesnumberTotal pages in the document. Text only.
subPageNumbernumberThe current subpage within its Page component.
subPageTotalPagesnumberTotal subpages of the Page component. Text only.
For <Text>, the render function runs twice — once during the wrapping pass, and again once the page count is known. On the first pass only pageNumber is available; totalPages, subPageNumber, and subPageTotalPages are undefined. Keep render functions pure and guard against the missing values.

Orphan & Widow Protection

vue-pdf avoids stranding single lines at a page boundary. Tune it with these props on any primitive:

PropTypeDefaultDescription
minPresenceAheadnumber0Prevents a page break between this element and its next sibling within n points.
orphansnumber2Minimum lines left at the bottom of a page. Text only.
widowsnumber2Minimum lines carried to the top of the next page. Text only.
vue
<template>
  <Document>
    <Page wrap>
      <!-- Never leave this heading stranded at the foot of a page -->
      <Text :min-presence-ahead="60" :style="{ fontSize: 16 }">
        Results
      </Text>

      <Text :orphans="3" :widows="3">
        A long body of text that will be split across pages...
      </Text>
    </Page>
  </Document>
</template>
minPresenceAhead is the fix for a heading rendering alone at the foot of a page. Set it to roughly the height of the content that must stay with it.

Debugging

Add debug to any primitive except <Document> to outline its content box, padding, and margin:

vue
<template>
  <Document>
    <Page size="A4" debug>
      <View debug :style="{ padding: 20, margin: 10 }">
        <Text debug>Content, padding and margin are outlined.</Text>
      </View>
    </Page>
  </Document>
</template>

Hyphenation

Line breaking uses the Knuth–Plass algorithm with English hyphenation patterns by default. Override the pattern set with Font.registerHyphenationCallback — it receives a word and returns its syllables:

ts
<script setup lang="ts">
import { Font } from '@vuepdf/renderer'
import { hyphenateSync as hyphenateDE } from 'hyphen/de'

// Return the syllables of a word as an array
Font.registerHyphenationCallback((word) =>
  hyphenateDE(word).split('\u00AD'),
)

// Or disable hyphenation entirely
// Font.registerHyphenationCallback((word) => [word])
</script>

See the Fonts guide for registration details and registerEmojiSource.

Usage With Express

On the server, renderToStream pipes straight into the response — no temporary file, no buffering the whole document:

ts
import express from 'express'
import { renderToStream } from '@vuepdf/renderer'
import MyDocument from './MyDocument.vue'

const app = express()

app.get('/report.pdf', async (req, res) => {
  const stream = await renderToStream(MyDocument)

  res.setHeader('Content-Type', 'application/pdf')
  stream.pipe(res)
  stream.on('end', () => console.log('Done streaming, response sent.'))
})

app.listen(3000)

If you need the bytes rather than a stream, use renderToBuffer.

Node cannot import a .vue file on its own. Outside Nuxt, run the server through a build step that compiles SFCs (Vite, tsup, or similar), or skip components entirely and build the document from @vuepdf/renderer/primitives — see Dynamic Content for that element shape. vue-pdf's own packages ship precompiled, so they need no plugin either way.

Rendering Large Documents in the Browser

Rendering is synchronous work on the main thread. Past roughly 30 pages that becomes a visible freeze, so move it into a web worker and keep the UI responsive:

pdf.worker.ts
// pdf.worker.ts
import { pdf } from '@vuepdf/renderer'
import MyDocument from './MyDocument.vue'

self.onmessage = async (event) => {
  const blob = await pdf(MyDocument, event.data.props).toBlob()
  self.postMessage(blob)
}
DownloadButton.vue
<script setup lang="ts">
const url = ref<string>()

onMounted(() => {
  const worker = new Worker(new URL('./pdf.worker.ts', globalThis._importMeta_.url), {
    type: 'module',
  })

  worker.onmessage = (event) => {
    url.value = URL.createObjectURL(event.data)
  }

  worker.postMessage({ props: { title: 'Big report' } })
})
</script>
Remember to URL.revokeObjectURL() the blob URL when you are done with it, and terminate the worker on unmount.

Math & Diagrams

Two optional packages extend vue-pdf: @vuepdf/math renders LaTeX expressions as vector paths, and @vuepdf/mermaid renders Mermaid diagrams. See Math & Diagrams.