Library / SDK
hrynko/vue-pdf-embed avatar
hrynko/vue-pdf-embed

vue-pdf-embed, a PDF renderer that leaves the interface to you

PDF embed component for Vue 2 and Vue 3

1,043 stars129 forksTypeScriptMIT

At a glance

What is it?
vue-pdf-embed is a Vue 3 component that renders PDF documents into a page, with a text layer, an annotation layer, password handling and a props table that documents the dependencies between its own options. It ships one dependency, pdfjs-dist, no peer dependencies and no required configuration, and it deliberately stops short of being a viewer, with the author pointing at a separate package for that.
Who is it for?
vue-pdf-embed fits a Vue 3 application that needs to display a document inside a page it already designs, and the props, events and slots give enough control to build a viewer without shipping a framework's opinionated chrome. It does not fit a Vue 2 codebase, since the current package is Vue 3 only, and it does not fit a team that wants a finished viewer, because the interface is the part this library leaves to you.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 15 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A renderer, and a pointer to the viewer

The readme is unusually clear about what the package is not. There is a note in the features list that says, in effect, if you are looking for a full PDF viewer, this library renders documents and leaves the UI to you, and for a ready-made viewer you should look at vue-pdfz from the same author.

That sentence is the whole design statement, and it is the reason to choose this package over its alternatives. A component that renders documents and a component that is a viewer differ in a way that matters for an application: the first is a building block you place inside a page you have already designed, and the second brings a toolbar, a page control, a zoom control and a layout you then have to override.

The features list is short and concrete. Controlled rendering of PDF documents in Vue apps. Handling of password-protected documents. A text layer, which makes the documents searchable and selectable. An annotation layer, for annotations and links. No peer dependencies or additional configuration required. And it can be used directly in the browser, with examples in the repository.

The controlled part of controlled rendering is the difference between embedding a browser plugin and embedding a component. You give it a source and a set of props, it renders, and it tells you what happened through events. Everything else is your application.

One dependency, and an optional style sheet

The dependency list in package.json contains a single entry, pdfjs-dist at version 6.4 and later, which is Mozilla's PDF rendering engine. There are no peer dependencies, so the package does not ask you to install a PDF library of your own or to align versions with another copy.

Installation is offered in the three ways a front-end package usually is:

shell
npm install vue-pdf-embed
shell
yarn add vue-pdf-embed
html
<script src="https://unpkg.com/vue-pdf-embed"></script>

The script tag route is what the claim about being usable directly in the browser refers to, and it is worth noting that it exists alongside a Vue 3 specific component, so the package is built in a way that the global build is useful.

Usage is a script setup block with the component imported, and the styles for the two layers imported optionally:

vue
<script setup>
import VuePdfEmbed from 'vue-pdf-embed'

// Optional styles
import 'vue-pdf-embed/dist/styles/annotationLayer.css'
import 'vue-pdf-embed/dist/styles/textLayer.css'

// Either URL, Base64, binary, or document proxy
const pdfSource = '<PDF_URL>'
</script>

<template>
  <VuePdfEmbed annotation-layer text-layer :source="pdfSource" />
</template>

The two style imports are marked optional, which is a small decision with a visible consequence. The text layer and the annotation layer are the parts that render selectable text and interactive form widgets, and they are styled with CSS that the component expects to find. If you skip the imports, the features still work but the appearance is not the intended one, and the fix is two import lines rather than a support question.

The props table documents its own dependencies

The props table is the real API documentation, and it is worth reading as a set rather than a list, because several entries constrain each other.

The source prop accepts a string, an object or a PDFDocumentProxy, and the comment in the usage example says the same thing in fewer words: either URL, Base64, binary, or document proxy. Passing a document proxy is the option that matters for a real application, because it lets you load the document yourself, with your own fetch logic, your own authentication and your own progress handling, and hand the ready document to the component.

The layer flags are annotationLayer and textLayer, both booleans. The forms prop, which renders interactive form fields, requires the annotation layer. The findController prop, which highlights text matches, requires the text layer. The linkService prop replaces the default document navigation service, and the default one is what emits the internal-link-clicked event, so swapping it means you also take over that event.

Then there is the layout and rendering group. Width and height are desired pixel dimensions, and the note on height says it is ignored if the width property is specified, which is a slightly odd asymmetry that is best handled by setting only one of them. The scale prop is a rendering resolution multiplier, and the description says it controls sharpness. The rotation prop accepts multiples of 90. The page prop takes a number or an array of numbers, from 1 to the last page, and displaying all pages is what happens when it is not specified. The imageResourcesPath prop points at the icons used by the annotation layer, and it has to be a URL or a path with a trailing slash.

The matchScrolling prop, on by default, decides whether the selected search match is scrolled into view.

Seven events, and the password flow in particular

The event list covers the whole lifecycle of a document, and reading it in order tells you what the component is doing at each moment.

The document lifecycle is loaded, which fires with the PDF document proxy when the document has finished loading, and rendered, which fires when the document has finished rendering. The failure counterparts are loading-failed and rendering-failed, both carrying an error object.

Then there is the progress event, which carries an object with loaded and total byte counts, so a progress bar is a few lines of code rather than a feature you have to build into the loading path.

The event that most integrations get wrong is password-requested. Its value is an object with a callback function and an isWrongPassword flag, and it fires when a password is required to display the document. The shape tells you what to do: the callback is what you call with the password the user typed, and the flag tells you whether the previous attempt was wrong, so you can show a different message the second time. Handling it is a piece of work, but the component has given you the two pieces of information the work needs.

The last event is internal-link-clicked, with the destination page number as its value, which is how a link inside the document is turned into navigation. If you replace the link service with your own, that event is yours to emit.

Slots for the content around a page

There are two slots, before-page and after-page, and each receives the page number as a prop. The description is content to be added before or after each page.

That is a small API, and it is the answer to the question that a rendering component always has to answer: what goes between the pages? A document viewer that only shows pages has no room for a separator, a caption, an advertisement slot, a section heading or a retry button for a page that failed. With these slots, the surrounding content is the application's own markup, styled by the application's own CSS, and the page number is passed along so the content can vary per page.

It is also what makes the component composable in a way a canvas-based renderer is not. If you need to interleave something between pages, you do not have to reach inside the component, and you do not have to reimplement the document handling to do it.

The alternative, which is to build the whole viewer yourself on top of pdfjs-dist, gives you more control still and costs a great deal more work. The slots are the middle ground the package is aiming at, and the readme's advice to look at a separate package for a complete viewer is the boundary of that middle ground.

Two bundles, a type check after the build, and Vitest

The build scripts in package.json show a project that is careful about what it ships.

The build runs Vite twice: once for the main bundle and once with a different configuration file, vite.config.essential.ts, and the second run passes --emptyOutDir false so it does not wipe the output of the first. The package exports then carry two entry points, the standard one at ./dist/index.mjs for import and ./dist/index.umd.js for require, plus a separate export for ./dist/index.essential.mjs with its own type declarations, and a styles export for ./dist/styles/ which is what the two optional CSS imports in the usage example resolve to.

A type check runs after the build, with vue-tsc against tsconfig.build.json, so a type error in the source fails the publish rather than reaching the registry. The types are shipped, with the main export pointing at ./dist/types/index.d.ts, which is what makes the component usable from TypeScript without a separate types package.

The test setup is Vitest with happy-dom for the DOM environment and @vue/test-utils for mounting components, and there is a vitest.config.ts at the root alongside the Vite configurations. The TypeScript configuration is split into several files, a base, a build, a dev, a lib, a node and a test variant, which is the usual way to keep the compiler options of a library, its tests and its tooling separate.

The commit hygiene is standard for a package of this size: husky with lint-staged for hooks, eslint in flat config form with the Vue and TypeScript presets, prettier with its own config, and an .nvmrc pinning the Node version used for development. The prepare script runs husky and then the build, so a fresh clone is ready to work on.

Where vue-pdf-embed is the wrong choice

The first limit is the Vue version. The package is only compatible with Vue 3, and the readme says for Vue 2 support to install vue-pdf-embed@1 and use the v1 documentation. That is a hard split: a team still on Vue 2 has to take an older major version with a different API and no path to the current one, or migrate the application first.

The second is the missing interface. If you wanted a viewer, this is not it, and the author says so by pointing at vue-pdfz. The work you inherit is a toolbar, page navigation, zoom, a download button, a print button, a sidebar for thumbnails or outline, and keyboard shortcuts, and each of those is a design decision you will have to make and defend.

The third is the cost of rendering in the browser. There is no server-side rendering path described, and a document is rendered in the browser, so a large PDF consumes memory and CPU on the user's machine. A document viewer that is embedded in a page for a handful of pages is fine; an application that lets a user browse a thousand-page report will feel it, and the props give you the tools to limit the pages, since the page prop takes an array and displaying all pages is the default.

The fourth is the layering of the optional features. The text layer, the annotation layer, the form rendering and the find controller all depend on each other in the way the props table spells out, and the annotation layer needs its icons from a path you configure. A feature that works in the demo and renders unstyled in your application is a configuration problem rather than a bug, but it is a support question for whoever uses the library after you.

The fifth is the release line. The versions are 2.1.5, 2.1.6 and 2.1.7, patch releases that have followed each other closely, and the project depends on a specific major of pdfjs-dist, so a future pdfjs release is a question for the maintainer rather than something you can resolve by upgrading your own dependencies.

Editorial conclusion

vue-pdf-embed fits a Vue 3 application that needs to display a document inside a page it already designs, and the props, events and slots give enough control to build a viewer without shipping a framework's opinionated chrome. It does not fit a Vue 2 codebase, since the current package is Vue 3 only, and it does not fit a team that wants a finished viewer, because the interface is the part this library leaves to you. Before adopting it, read the prop dependencies, since forms requires the annotation layer and the find controller requires the text layer, and decide how you will handle the password-requested event, which is the part of a PDF workflow that most integrations get wrong. The licence is MIT, version 2.1.7 was published on 2026-10-10, and the last push was on the same day.

Frequently asked questions

How do I install vue-pdf-embed?

With `npm install vue-pdf-embed`, or `yarn add vue-pdf-embed`, or with a script tag pointing at the unpkg build. The package has no peer dependencies and needs no additional configuration, and the only runtime dependency is pdfjs-dist.

Does vue-pdf-embed support Vue 2?

Not in its current form. The readme states that the package is only compatible with Vue 3, and that Vue 2 users should install vue-pdf-embed@1 and refer to the v1 documentation, which lives in a separate branch of the repository.

What can I pass as the source of a document?

The source prop accepts a string or an object, and the usage example says either URL, Base64, binary, or document proxy. Passing a PDFDocumentProxy lets you load the document yourself and reuse the same document in several places.

How do I handle a password-protected PDF?

Listen for the password-requested event. Its value is an object with a callback function and an isWrongPassword flag, so you can ask the user for a password, pass it to the callback and show a different message when the previous attempt was wrong.

Does vue-pdf-embed include a viewer interface?

No. The library renders documents and leaves the UI to you, and the readme points at vue-pdfz, from the same author, for a ready-made viewer. You can also add content around pages with the before-page and after-page slots.

Official sources

  1. hrynko/vue-pdf-embed on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/hrynko-vue-pdf-embed.svg)](https://hysenlabs.com/projects/hrynko-vue-pdf-embed)

Was this useful?