DEV Community

Cover image for How to Add PDF Annotations in Vue.js: PDF.js, vue-pdf-embed, @tato30/vue-pdf, and Vue PDF Viewer (2026)
Kittisak Ma
Kittisak Ma

Posted on

How to Add PDF Annotations in Vue.js: PDF.js, vue-pdf-embed, @tato30/vue-pdf, and Vue PDF Viewer (2026)

Vue PDF Viewer didn't start as an annotation tool. It started as a viewer. Render a PDF, give it a toolbar, let people zoom and search and print. That's what people asked for first, and for its first year that was the whole product.

Then, our users wanted to mark up the documents they were reading. So we added the first annotation tools in September 2025: highlight, free text, and image. The requests didn't stop there, because a highlighter alone can't say everything people need to say on a document. A highlight means "pay attention here," an underline marks a reference, a strikethrough means "cut this." Force all three through one yellow highlight and you lose the distinction, so underline and strikethrough followed in February 2026. I worked on that plugin. That's the disclosure, and it's also why I can tell you where each option's seams are, ours included.

Here's the part worth knowing before you write any code. Vue PDF Viewer renders with PDF.js, like most of the Vue ecosystem, so PDF.js's own annotation editor was the obvious place to start on the creation side. It creates free text, ink, and stamps, and it has a highlight tool. It has no underline tool and no strikethrough tool. That gap has held for years. And the highlight it does give you is free-form rather than snapping cleanly to a text selection. So the text-selection toolkit ended up being something we built ourselves.

"Annotations" sounds like one feature. It's really three separate jobs. This walks through how to add PDF annotations in Vue.js across four approaches (PDF.js directly, vue-pdf-embed, @tato30/vue-pdf, and Vue PDF Viewer), plus a couple of escape hatches for when none of them fit.

Versions tested: Vue 3.5.x, pdfjs-dist 5.4.530, vue-pdf-embed 2.1.4, @tato30/vue-pdf 2.1.0, @vue-pdf-viewer/viewer 4.3.0 with @vue-pdf-viewer/annotation 1.6.0, on Chrome and macOS.

Rendering, creating, and persisting are three different jobs

There are three problems hiding inside the word "annotation," and conflating them is the most expensive mistake you can make. Someone drops a marked-up PDF into each demo, sees every highlight render perfectly, picks the smallest library, and finds out two sprints later that it can't create a highlight at all.

The first is rendering: a PDF may already contain annotations (highlights, notes, stamps someone added in Acrobat), and your viewer has to display them. This is the easy problem. Every library here handles it, because PDF.js handles it.

The second is creating: your user selects a sentence and highlights it, draws on the page, leaves a comment. Different problem. It needs editing tools, a toolbar, state, and a way to turn a drag of the mouse into an annotation the PDF format understands. This is where the libraries split apart, and where most of them stop.

The third is persisting: the highlight your user just made has to survive a reload, a round-trip to your backend, and ideally open correctly in someone else's PDF reader. Teams tend to find this one last, usually after shipping, when it's too late to change the first two choices.

A library can render annotations perfectly and still create none of its own, or let users create them and then save them where no other reader can read them. Decide which of the three you need before you evaluate anything:

Approach Render existing Create new Persist & export
PDF.js (direct) Yes Partial, no underline/strikethrough DIY, and fragile
vue-pdf-embed Yes No —
@tato30/vue-pdf Yes Yes (beta, single-page) DIY
Vue PDF Viewer Yes Yes (5 types) Save / print / download, XFDF in and out

We'll take them one problem at a time, starting with the easy one.

Rendering existing PDF annotations costs you one prop

Someone marks up a contract in Acrobat. Three highlights, a strikethrough through a clause they want gone, an ink scribble in the margin, one sticky note. They email you the file and your Vue app has to show all of it.

That's one prop.

PDF.js ships an annotation layer that walks the annotation objects already stored in the file and draws them over the rendered page. Both lightweight Vue libraries expose it the same way, as a boolean.

vue-pdf-embed (hrynko's package on npm, not vue-pdf by FranckFreiburger, which is Vue 2 only and abandoned since 2021):

<script setup>
import VuePdfEmbed from 'vue-pdf-embed'
import 'vue-pdf-embed/dist/styles/annotationLayer.css'
</script>

<template>
  <VuePdfEmbed source="/marked-up.pdf" annotation-layer />
</template>
Enter fullscreen mode Exit fullscreen mode

@tato30/vue-pdf does the same job with a per-page API:

<script setup>
import { VuePDF, usePDF } from '@tato30/vue-pdf'
import '@tato30/vue-pdf/style.css'

const { pdf } = usePDF('/marked-up.pdf')
</script>

<template>
  <VuePDF :pdf="pdf" annotation-layer />
</template>
Enter fullscreen mode Exit fullscreen mode

If you forget the stylesheet, the layer mounts invisibly, which is the one catch here and costs an afternoon the first time. Otherwise you get the whole subtype set: link, text, highlight, underline, strikeout, squiggly, ink, stamp, free text. Underline and strikeout included, which matters shortly.

Our own changelog puts a date on just how far rendering ran ahead of creating. Vue PDF Viewer v1.5.0 shipped on 29 October 2024 and rendered highlight, strikeout, underline, squiggly, ink and free text. The first annotation a user of ours could create shipped in September 2025, eleven months later, and it was highlight, free text and image only. Underline and strikethrough creation didn't release until February 2026, sixteen months after we could already draw them on a page.

Creating PDF annotations is where the libraries come apart

Four ways to let a user make a mark, ordered by how much ships versus how much you build.

The PDF.js annotation editor

Everything below except pdf-lib is standing on this, so start here. PDF.js has an editor layer, the same one behind the markup tools in Firefox's built-in viewer. You turn it on and set a mode:

import { AnnotationEditorType } from 'pdfjs-dist'

// on a PDFViewer instance you've already set up yourself
pdfViewer.annotationEditorMode = { mode: AnnotationEditorType.HIGHLIGHT }

// FREETEXT | HIGHLIGHT | INK | STAMP, plus a signature editor in recent builds.
// There is no AnnotationEditorType.UNDERLINE.
// There is no AnnotationEditorType.STRIKEOUT.
Enter fullscreen mode Exit fullscreen mode

That enum is the entire story. Free text, highlight, ink, stamp, and a signature editor built on top of stamp in recent builds. No underline tool. No strikethrough tool. Not on any version, including Mozilla's unreleased development code. This isn't a "not yet," it's the shape the project has had for years, and the annotations it refuses to author are the same ones it renders without complaint if they're already in the file.

On versions: if you're building straight on PDF.js, 6.1.200 is the current release, and none of the above changes. We test against the 5.x line ourselves, because Vue PDF Viewer's peer range (^5.4.530) excludes 6.x by construction.

The highlight tool you do get is free-form. There's a text-selection mode in there, and on a clean single-column page it behaves. Give it a two-column layout with a table in the middle, though, and the snapping gets too unreliable to put in front of users. That's our experience building on it rather than a documented limitation, so weigh it as such. What you can promise is "highlight a region," not "highlight that sentence."

Two more things fall on you. The marks the editor makes are HTML overlays on top of the page, not part of the PDF itself. Draw a highlight in Mozilla's demo viewer, open DevTools, and here is what you actually made:

<div class="annotationEditorLayer highlightEditing">
  <div id="pdfjs_internal_editor_0" class="highlightEditor selectedEditor"
       role="mark" aria-label="Highlight editor"
       style="left: 51.69%; top: 49.9%; width: 39.36%; height: 3.99%;">
    <div class="internal" style="clip-path: url(#clip_path_0);"></div>
    <div class="editToolbar" role="toolbar">…</div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

A percentage-positioned <div> with a clip-path, sitting in a sibling layer above canvasWrapper and textLayer. Note the id: pdfjs_internal_editor_0. Internal, as the name says. It's a DOM node in your app, not a byte in the PDF. Saving it into the file where another reader can see it is a separate job (that's Section 4's problem). And the toolbar is yours to build. All of it: the tool picker, the colour swatches, the delete button, and the state that tracks which tool is active.

@tato30/vue-pdf's editor-layer is that same editor in Vue clothing

This is not a second, separate approach. It's the PDF.js editor from above, wrapped in a Vue component so you're setting a prop instead of managing a viewer instance. That wrapping earns @tato30 a distinction worth naming: it's the only lightweight Vue PDF library that creates annotations at all.

<template>
  <VuePDF :pdf="pdf" :page="page" editor-layer />
</template>
Enter fullscreen mode Exit fullscreen mode

You get the primitives in Vue components: free text, highlight, ink, stamp, plus comments. If drawing is what you need, ink is the primitive here, and it works.

Two limits worth knowing before you commit. It's beta-stage, and interactions are limited to single pages, which matters the moment your review UI scrolls. A contract clause that starts at the bottom of page 4 and finishes at the top of page 5 becomes two separate annotations on two <VuePDF> instances, and stitching them back into one highlight is your code. TaTo30's release notes are unusually straight about the ceiling: other annotation types won't be supported as long as pdf.js doesn't support them. That's the honest answer, and it's also exactly the problem. The library inherits PDF.js's gaps along with its primitives, so underline and strikethrough aren't here either.

You still build the UX. What you save is the wiring, not the toolbar.

pdf-lib, for when you just need it burned in

Different shape entirely. pdf-lib has no viewer and no UI. It's a framework-agnostic library that opens a PDF, draws into it, and hands you back bytes. Useful server-side, or as a pre-render step before the document ever reaches your Vue component.

import { PDFDocument, rgb } from 'pdf-lib'

const pdfDoc = await PDFDocument.load(existingBytes)
const page = pdfDoc.getPages()[0]

page.drawText('APPROVED', { x: 50, y: 700, size: 24, color: rgb(0.8, 0, 0) })

const stamped = await pdfDoc.save()
Enter fullscreen mode Exit fullscreen mode

The catch is what those marks are. They're content drawn into the page, not standard PDF annotation objects, so they won't appear in Acrobat's or Preview's annotation panel and nobody can select, edit or delete them afterward. That's the point if you're stamping an approval watermark. It's the wrong tool if a human needs to take the mark back off.

Maintenance, honestly: v1.17.1 is the latest, released November 2021. Nothing since. It still works, and plenty of production pipelines run on it, but you're adopting a frozen dependency.

The Vue PDF Viewer annotation plugin

Our option, and the disclosure from the top of this article applies. Activation is one prop:

<script setup>
import { VPdfViewer } from '@vue-pdf-viewer/viewer'
import VPdfAnnotationPlugin from '@vue-pdf-viewer/annotation'
</script>

<template>
  <VPdfViewer src="/marked-up.pdf" :plugins="[VPdfAnnotationPlugin()]" />
</template>
Enter fullscreen mode Exit fullscreen mode

That gets you a create UI for highlight, underline, strikethrough, free text and image (the docs call the image one Image Overlay). Underline and strikethrough are in there because we wrote the text-selection toolkit ourselves rather than inheriting PDF.js's editor, which is the whole reason this article exists. Highlight, free text and image came first in September 2025; underline and strikethrough followed by request in February 2026.

Now the edges, because you'll find them anyway. Comments are view-only today: the panel displays comment threads already in the file, and there's no create button. Drawing, shapes and a stamp palette aren't in the plugin today (the image tool covers arbitrary user-supplied images, not preset approval stamps), and e-signatures and form-field authoring aren't either; I won't even tell you those two are planned, because they aren't. If ink drawing is your core requirement today, the honest answer is @tato30's editor-layer or raw PDF.js, not this plugin.

Persisting PDF annotations is the part that bites you later

Your user highlights a clause and closes the tab. Now what? That question decides more than rendering and creating combined. There are three sub-questions inside it. Where does the annotation data live, how is it serialized (as XFDF, the XML standard other readers can import, or as JSON only your own app understands), and can anything besides your app read it back?

PDF.js editor. You save the annotations yourself and store them somewhere. Then there's the problem Section 3 handed forward: those editor overlays are DOM nodes, and getting them into the file is a job PDF.js won't do for you. Your highlights work in your app. Open the same file in Preview and they were never there.

@tato30/vue-pdf. Same editor, same problem, plus the single-page caveat shaping how you batch what you save.

pdf-lib. The marks are drawn straight into the page, which is the point: there's no separate save step, they're just part of the file pdf-lib hands back. No round-trip either. They're page content now, not annotations, so there's nothing to export, edit or remove.

Vue PDF Viewer. Save, print and download with annotations baked in, plus XFDF export and import through the XFDF Controller in the Instance API. That shipped on 3 July 2026 in annotation v1.6.0 and viewer v4.3.0.

// `viewer` is a template ref on the <VPdfViewer> component
// export the current annotations as an XFDF string, store it wherever you like
const xfdf = await viewer.value.annotationXfdfControl.exportToXfdf()

// later, on a fresh mount of the same document (importFromXfdf is synchronous)
const { annotations, skipped, errors } = viewer.value.annotationXfdfControl.importFromXfdf(xfdf)
Enter fullscreen mode Exit fullscreen mode

What comes back is a document, not a private blob. A single highlight serializes to something like this:

<?xml version="1.0" encoding="UTF-8"?>
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
  <f href="contract.pdf"/>
  <annots>
    <highlight page="0" rect="72.0000,690.2000,331.5000,704.8000"
               color="#FFE066" opacity="0.4"
               name="6f1c…" title="Vue PDF Viewer"
               date="D:20260703T101500" creationdate="D:20260703T101500"
               flags="print"
               coords="72.0000,704.8000,331.5000,704.8000,72.0000,690.2000,331.5000,690.2000"/>
  </annots>
</xfdf>
Enter fullscreen mode Exit fullscreen mode

A zero-based page index, the colour, and the quad points of the highlighted text, in a schema Acrobat and Preview already parse. The <f href> names the document the marks belong to; coords is an attribute, not a child element, because that's the one spelling Acrobat actually reads. That's why the string is worth storing in your own database: it's readable by things that aren't your app.

One precision, because I'd rather you hear it here than in a support ticket: this is programmatic, not a button. There's no end-user Export XFDF item in the toolbar. You call it from the Instance API and you decide when. If what you want is a file on the user's disk rather than a string, exportToXfdf({ download: true }) does that.

And it's not only the marks your users made. Export walks the whole document and merges what's in the editor with the annotations already in the file when it loaded, so a PDF that arrives with ink from someone's tablet and squares from a reviewer's Acrobat round-trips those too. Thirteen annotation types survive the trip, not just the five the toolbar can create.

The reason XFDF matters isn't that it's ours. It's that a standards-based round-trip is the exact thing PDF.js's overlays can't do and the exact thing pdf-lib gives up by drawing straight into the page. A highlight your reviewer makes in your Vue app opens in their lawyer's Acrobat. That's the whole argument, and it's a format argument rather than a product one.

Signatures deserve a mention, since for many people they're the whole reason for using annotations. Free-text and image annotations can serve as lightweight signatures, and persistence is what makes one stick: an image that vanishes on reload isn't a signature. But none of these lightweight Vue options is a real e-signature workflow product.

When you actually need the enterprise tier

Two options sit above everything discussed so far, and it would be dishonest to leave them out.

Syncfusion's EJ2 Vue PDF Viewer ships text markup, shapes, stamps, ink, free text, sticky notes, measurement tools, form filling, e-signatures and redaction. Nutrient (formerly PSPDFKit) covers 17+ annotation types with create, edit and remove available through the UI or programmatically, XFDF and JSON round-trips, and real-time collaboration sync.

Both are commercial, both are priced accordingly, and both are genuinely more capable than anything else on this page. Reach for them when annotation breadth, compliance or multi-user collaboration is the product. Not when markup is a supporting feature rather than the core of the product or system.

Picking a path

Match the tool to what you're actually building, not to the shortest README.

Displaying markup that already exists. vue-pdf-embed, or @tato30/vue-pdf if you want per-page control. One prop, MIT, done this afternoon. Reaching past this tier for a read-only viewer is how projects acquire dependencies they regret.

Lightweight DIY markup, and you're willing to build the UI. The PDF.js editor layer directly, or @tato30's editor-layer for the Vue wrapping. You get free text, highlight, ink and stamp. You write the toolbar, the state, and the persistence. Budget real time for the last one.

Server-side or pre-render stamping, no UI at all. pdf-lib. Frozen since 2021 and fine for the job, as long as the job is burning marks in permanently.

Shipping a review or markup feature without building the toolbar. Vue PDF Viewer, and here the disclosure at the top applies: it's the only option that ships underline and strikethrough creation, because PDF.js has no tool to create them and everything downstream inherits that gap. Add the XFDF round-trip and the toolbar you'd otherwise be writing, and the trade is a commercial licence against the weeks in Section 3. If your project can't take a paid licence, that trade doesn't exist and @tato30's editor-layer is your answer.

Compliance, collaboration, or 17+ annotation types. Syncfusion or Nutrient.

Here's the full picture, pdf-lib included:

PDF.js (direct) vue-pdf-embed @tato30/vue-pdf pdf-lib Vue PDF Viewer
License Apache 2.0 MIT MIT MIT Commercial (perpetual)
Latest version 6.1.200 (5.4.530 used here) 2.1.5 (Jun 2026) 2.1.0 (May 2026) 1.17.1 (Nov 2021) viewer 4.3.0 + annotation 1.6.0 (3 Jul 2026)
Render existing Yes Yes (annotation-layer) Yes (annotation-layer) No (not a viewer) Yes
Create (types) Free text, highlight, ink, stamp None Free text, highlight, ink, stamp, comments Text, image, shapes (as page content) Highlight, underline, strikethrough, free text, image
Create UI shipped No, primitives only n/a Partial, beta No UI at all Yes
Persist / export DIY, overlays may not reach the PDF binary n/a DIY, same overlay caveat Burned in, no round-trip Save / print / download + XFDF import/export (programmatic)
Known limits No underline or strikethrough tool, free-form highlight Render only Beta, single-page interactions Not standard annotation objects, unmaintained Comments view-only, no stamps or e-signatures
Vue 3 Manual setup Yes Yes Framework-agnostic Yes (purpose-built)

Versions verified 16 July 2026. Releases move. Re-check before you commit to any of this.

What to decide before you install anything

Rendering is one prop. Creating is where the libraries come apart, and the gap runs deeper than any of them advertise: PDF.js has no underline or strikethrough tool, so nothing built on PDF.js has one either unless someone sat down and wrote it. Persisting is the one that surfaces after launch, when someone opens your reviewed contract in Acrobat and finds a clean page.

So the useful question isn't which library is best. When you ask how to add PDF annotations in Vue, you're asking about three jobs, and the one that matters is whichever you're actually signing up for. Answer that first and most of this page collapses into one row of the table.

If it turns out you need the create UI and the round-trip, the annotation plugin docs and live demo are the fastest way to check whether it fits, including the parts it doesn't do.

Top comments (0)