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":
<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:
<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:
<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:
<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:
<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:
<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>| Prop | Type | Default | Description |
|---|---|---|---|
title* | string | — | The label shown in the reader's outline. |
top | number | 0 | Y coordinate the reader scrolls to. |
left | number | 0 | X coordinate the reader scrolls to. |
zoom | number | — | Zoom level applied when the bookmark is followed. |
fit | boolean | — | Jump to the start of the page rather than a coordinate. |
expanded | boolean | — | Show this node already expanded in the outline tree. |
Dynamic Content
Pass a function to the render prop of <Text> or <View> to generate content that depends on where it lands:
<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:
<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><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:
<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>| Prop | Type | Default | Description |
|---|---|---|---|
pageNumber | number | — | The current page number. |
totalPages | number | — | Total pages in the document. Text only. |
subPageNumber | number | — | The current subpage within its Page component. |
subPageTotalPages | number | — | Total subpages of the Page component. Text only. |
<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:
| Prop | Type | Default | Description |
|---|---|---|---|
minPresenceAhead | number | 0 | Prevents a page break between this element and its next sibling within n points. |
orphans | number | 2 | Minimum lines left at the bottom of a page. Text only. |
widows | number | 2 | Minimum lines carried to the top of the next page. Text only. |
<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:
<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:
<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:
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.
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
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)
}<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>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.