Displaying PDFs in Vue 3 Applications
Embedding a PDF inside a Vue 3 SPA sounds straightforward—until you hit your first CORS error, discover the browser renders it differently on Safari, or realize your 200-page document is blocking the main thread. This article covers the three practical approaches developers actually use for displaying PDFs in Vue 3, with honest trade-offs for each.
Key Takeaways
- The three main approaches for displaying PDFs in Vue 3 are native browser embeds (
<iframe>/<embed>), direct PDF.js integration, and Vue-specific wrapper components. - Native embeds cost zero bundle size but offer no control over styling, toolbars, or cross-browser consistency.
- PDF.js gives you full rendering control but adds a noticeable client-side payload—lazy-load it with dynamic
import()to keep initial load times fast. - Vue PDF wrappers like
vue-pdf-embedreduce boilerplate significantly at the cost of some customization flexibility. - Correct worker configuration and lazy loading are the two steps that prevent the most common PDF-related issues in Vue apps.
The Three Core Approaches
1. Native Browser Embed: <iframe> or <embed>
The fastest way to get a PDF on screen is to hand it off to the browser’s built-in PDF renderer.
<template>
<iframe
:src="pdfUrl"
width="100%"
height="700px"
style="border: none"
/>
</template>
<script setup lang="ts">
const pdfUrl = '/documents/report.pdf'
</script>
When it works well: Internal tools, admin dashboards, or any context where UI consistency isn’t critical and the PDF is served from the same origin.
The real limitations:
- Zero control over the toolbar, zoom controls, or theming
- Behavior varies across browsers—Chrome, Firefox, and Safari each render differently
- Mobile browsers often download the file instead of displaying it inline
- Loading state is invisible to your Vue component, so you can’t show a spinner or handle errors gracefully
If you’re loading a PDF from a different domain, the browser will block the request unless the server sends the correct Access-Control-Allow-Origin headers. A same-origin proxy endpoint on your backend is the cleanest fix.
2. PDF.js with Vue 3: Direct Integration
PDF.js is Mozilla’s open-source PDF rendering engine. As of the 5.x release series, it ships as an ESM-first package. You import from build/pdf.mjs and load a separate worker (pdf.worker.mjs) to keep rendering off the main thread.
npm install pdfjs-dist
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import * as pdfjsLib from 'pdfjs-dist/build/pdf.mjs'
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.mjs',
import.meta.url
).toString()
const canvasRef = ref<HTMLCanvasElement | null>(null)
onMounted(async () => {
const pdf = await pdfjsLib.getDocument('/documents/report.pdf').promise
const page = await pdf.getPage(1)
const viewport = page.getViewport({ scale: 1.5 })
const canvas = canvasRef.value!
const ctx = canvas.getContext('2d')!
canvas.height = viewport.height
canvas.width = viewport.width
await page.render({ canvasContext: ctx, viewport }).promise
})
</script>
<template>
<canvas ref="canvasRef" />
</template>
What you gain: Full control over rendering, page navigation, zoom, and theming. PDF.js supports standard annotations and AcroForm fields, though form and annotation behavior can depend on rendering configuration.
What you should know:
- The
pdfjs-distpackage adds a noticeable payload to your client bundle. Use dynamicimport()to lazy-load it. - XFA-based forms (common in older government PDFs) have limited support and may not render correctly.
- For large documents, PDF.js can fetch PDFs in chunks when the server supports HTTP range requests (
Accept-Ranges: bytes), which can improve perceived performance for large files. - Worker configuration is the most common setup issue in Vite projects. The
new URL(..., import.meta.url)pattern shown above resolves the worker path correctly at build time.
Discover how at OpenReplay.com.
3. Vue PDF Wrappers
Several maintained Vue 3 components wrap PDF.js and expose a simpler component API. vue-pdf-embed is one actively maintained option that handles worker setup, page rendering, and reactive prop updates for you.
<script setup lang="ts">
import VuePdfEmbed from 'vue-pdf-embed'
</script>
<template>
<VuePdfEmbed source="/documents/report.pdf" />
</template>
The trade-off is less granular control in exchange for significantly less boilerplate. These wrappers are a good fit when you need a working Vue 3 PDF viewer quickly and don’t need to customize the rendering pipeline.
Note: Some older packages like
vue3-pdf-appare no longer actively maintained. Evaluate any wrapper’s maintenance status and compatibility with modern bundlers like Vite before adopting it.
Choosing the Right Approach
| Approach | Bundle Cost | Customization | Best For |
|---|---|---|---|
<iframe> / <embed> | 0 KB | None | Quick embeds, same-origin files |
| Direct PDF.js | Client-side payload | Full | Custom viewers, large docs |
| Vue wrapper | Similar to PDF.js | Moderate | Faster setup, standard use cases |
Conclusion
For most Vue 3 applications, the decision is straightforward: use <iframe> for simple same-origin embeds, reach for a maintained Vue wrapper when you need a working viewer without much setup, and integrate PDF.js directly when you need full control over rendering, navigation, or performance for large documents. Whichever path you choose, configure the PDF.js worker correctly and lazy-load the library. Those two steps alone will prevent the most common issues developers run into when embedding PDFs in Vue apps.
FAQs
The browser blocks cross-origin PDF requests unless the server includes the Access-Control-Allow-Origin header in its response. The most reliable solution is to set up a same-origin proxy endpoint on your own backend that fetches the PDF and serves it to your Vue app. This avoids relying on third-party server configurations you may not control.
Vite handles asset URLs differently from Webpack. Use the new URL with import.meta.url pattern to resolve the worker path at build time. For example, set workerSrc to new URL of pdfjs-dist/build/pdf.worker.mjs with import.meta.url, then call toString. This ensures Vite processes the worker file correctly during both development and production builds.
Yes. After loading the document, loop from 1 to pdf.numPages, call getPage for each page number, create a canvas element for each, and render them sequentially or in parallel. For very large documents, consider rendering only the visible pages and loading others on scroll to avoid high memory usage and slow initial render times.
Use a wrapper like vue-pdf-embed if you need a standard viewer with minimal setup and do not require deep customization. Integrate PDF.js directly when you need full control over rendering, custom navigation, theming, or performance optimizations like lazy page loading. Wrappers add a thin layer over PDF.js, so the bundle cost is nearly identical either way.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.