EmbedPDF

Documents

Give the viewer a document with src: a URL, a File, or bytes.

{ src: '/reports/2026-q3.pdf' }

A relative URL is relative to your page. A URL on another site needs CORS headers that allow yours, like any fetch.

Open the file someone picks#

src follows its prop: give it another document, and the viewer opens that one in its place.

pick-file.vue
<script setup lang="ts">
import { ref } from 'vue';
import { PDFViewer } from '@embedpdf/viewer-vue';

const file = ref<File | null>(null);
const pick = (event: Event) => (file.value = (event.target as HTMLInputElement).files?.[0] ?? null);
</script>

<template>
  <input type="file" accept="application/pdf" @change="pick" />
  <PDFViewer :src="file" style="height: 80vh" />
</template>

People can also open files themselves: drop a PDF on the viewer, or use Open in the document menu. Both open it as a new tab.

Several documents#

Give documents instead of src to open several at once. Each one gets a tab:

{
  documents: [
    { src: '/contract.pdf', name: 'Contract' },
    { src: '/appendix.pdf', name: 'Appendix A' },
    { src: '/scan.pdf', name: 'Signed scan', active: true },
  ],
}
FieldWhat it isDefault
srcA URL, a File, or bytesnone
nameWhat its tab showsthe file’s name
passwordIts password, if it has onenone
activeShow this one firstthe first document

Loading live preview…

tabs.vue
<script setup lang="ts">
import { PDFViewer, type ViewerConfig } from '@embedpdf/viewer-vue';

const welcome = '/pdfs/welcome.pdf';
const article = '/pdfs/article.pdf';
const report = '/pdfs/report.pdf';

const config: ViewerConfig = {
  documents: [
    { src: welcome, name: 'Welcome' },
    { src: article, name: 'Engineering blog' },
    { src: report, name: 'Q3 report', active: true },
  ],
};
</script>

<template>
  <PDFViewer v-bind="config" style="height: 100%" />
</template>

Try it: the Q3 report shows first, as active says. Switch to the welcome guide and the engineering blog from the tabs above the toolbar, and close the one showing with its ×.

The tabs show while more than one document is open. To show them always or never, set layout.tabs('always') or layout.tabs('never') in your layout. onDocumentChange tells you which document is showing each time that changes, and null once none is.

Passwords#

A document with a password opens locked, and the viewer asks for it. When you know it, pass it along and nobody is asked:

{ documents: [{ src: '/payroll.pdf', password: secret }] }

A wrong password asks again. A document that can’t be opened at all shows why in its tab, with a Try again button.

Files that need a sign-in#

When your server wants a header or a cookie for a file, give the viewer your own fetch. Every document at a URL downloads through it, from src, documents or { kind: 'url' } in code. It gets the URL, and an init with the document’s cancel signal to pass on:

{
  src: '/api/files/contract-42.pdf',
  fetch: (url, init) =>
    fetch(url, { ...init, headers: { Authorization: `Bearer ${getToken()}` } }),
}

For your cookies on another site, add credentials: 'include' instead; that site’s CORS headers have to allow it.

The tab still shows the document loading, and closing it stops the download. A server that answers 401 or 403 shows in the tab as not allowed, and Try again calls your fetch again, so a token it reads when it’s called is a fresh one. fetch follows its prop: the next download uses a new one, and nothing reopens. The viewer’s own files, such as the engine’s, never go through it, so your token goes only to your own server.

Open and close documents from code#

viewer.documents opens, closes and switches documents. It’s the documents plugin, the same as in headless:

const { document } = await viewer.documents.open(
  { kind: 'url', url: '/invoice-1042.pdf' },
  { name: 'Invoice 1042' },
);
 
viewer.documents.setActive(document.id);
viewer.documents.close(document.id);

open() resolves once the document is ready, and makes it the active one.

Download and print#

The document menu has Download and Print, and Ctrl/Cmd+S and Ctrl/Cmd+P work while the viewer has focus. A download includes every annotation, form value and signature.

Print opens a dialog: every page, the current page or pages such as 1-3, 5, and whether annotations print. Nothing is made while the user chooses: its Print button prepares those pages, shows how far it is, and opens the browser’s print dialog when they’re ready. Safari prints only from a tap, so there the dialog says the pages are ready and prints from the next tap on Print. Pages print as the PDF says they print: an annotation or a form field that is set not to print stays off the paper. On a desktop browser with its own PDF viewer, a document that allows high-quality printing and downloading prints as a PDF, with sharp text and lines. Everywhere else the pages print as pictures, at 300 DPI, or at 150 DPI when the document allows only low-resolution printing; the dialog says which.

When the scope doesn’t allow printing, or you turn it off, Print isn’t there, and Ctrl/Cmd+P says so instead of printing the web page.

To run them from your own button, use the commands document:download and document:print, or call them from code. The command opens the dialog; documents.print() prints every page without it:

viewer.commands.execute('document:print');
await viewer.documents.print();

Take a snapshot#

Take a Snapshot, in the document menu, takes part of a page as a picture, like Acrobat’s snapshot tool. Drag a box over a page, or click a page for all of it. The box stays, with a bar under it:

  • Copy puts the picture on the clipboard, to paste into a message, a document or a slide. Ctrl/Cmd+C does the same. Once it’s copied, the box goes, as selected text does.
  • Save image downloads it as a PNG, named after the document and the page.

Escape lets the box go, and so does switching to another tool. The picture is the box at twice its size in points, 144 DPI, with the annotations and form fields the user sees. For bigger pictures, raise the render plugin’s snapshot.scale:

{
  plugins: {
    render: { snapshot: { scale: 3 } },
  },
}

A PDF that doesn’t allow copying doesn’t allow snapshots either, as in Acrobat: Take a Snapshot isn’t in the menu. To leave snapshots out everywhere, disable: ['snapshots']. From your own button, run tool:snapshot.

Save to your server#

viewer.documents.download() gives you the PDF with every change, as bytes, without a download dialog. Make it a button of your own:

{
  commands: [
    {
      id: 'acme:save',
      label: 'Save',
      icon: 'save',
      shortcut: 'Mod+S',
      run: async ({ viewer, documentId }) => {
        const bytes = await viewer.documents.download(documentId);
        await fetch(`/api/documents/${documentId}`, { method: 'PUT', body: bytes });
      },
    },
  ],
  layout: (layout) => layout.replace('document:download', 'acme:save'),
}

Mod+S now saves to your server instead of downloading, because a command of yours wins over a built-in one with the same shortcut.

Warn before changes are lost#

When someone closes a tab or leaves the page with unsaved changes, the viewer asks first. Unsaved means changed since it opened or since the last download: anything a download would carry, whether the user changed it or your code did, such as form values from importValues() or a form reset(). viewer.documents.get().hasUnsavedChanges tells you the same, and onUnsavedChangesChanged fires when it changes:

viewer.documents.onUnsavedChangesChanged(({ documentId, hasUnsavedChanges }) => {
  saveButton.disabled = !hasUnsavedChanges;
});

When your app keeps the changes itself, or nothing is meant to be kept, as at a kiosk that starts over for the next visitor, leave the browser’s question out. Closing a tab still asks:

{ warnBeforeLeaving: false }

warnBeforeLeaving is read once, when the viewer starts.

Good to know#

Big files#

The local engine needs the whole file before the first page shows, so a large scan takes as long as its download. The toolbar is there at once; only the pages wait.

Pages that take a moment#

A page whose picture takes more than 400 milliseconds to arrive shows a spinner, in the main view and in the thumbnails, until it’s there; a page that shows quickly shows none. Getting sharper as you zoom in isn’t loading, so it never spins. A page that can’t load says so. Change the wait with plugins: { render: { slowAfterMs: 200 } }, and restyle the spinner or hide it with its part, page-loading.

Where the bytes go#

With the local engine, a document never leaves the browser: no upload, no server. Each document runs in the engine’s worker, so a heavy page doesn’t freeze your app.

Every documents method#

viewer.documents is the headless documents plugin. Its methods, state and events are listed on Opening documents, Several documents and Saving.

Was this page helpful?

Your feedback goes directly to the documentation team.