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.
<input type="file" accept="application/pdf" id="file" />
<embedpdf-viewer id="viewer" style="height: 80vh"></embedpdf-viewer>
<script type="module">
import 'https://cdn.jsdelivr.net/npm/@embedpdf/viewer@3/dist/embedpdf.js';
const viewer = document.getElementById('viewer');
document.getElementById('file').addEventListener('change', (event) => {
viewer.src = event.target.files[0];
});
</script>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 },
],
}| Field | What it is | Default |
|---|---|---|
src | A URL, a File, or bytes | none |
name | What its tab shows | the file’s name |
password | Its password, if it has one | none |
active | Show this one first | the first document |
Loading live preview…
<embedpdf-viewer id="viewer" style="height: 100%"></embedpdf-viewer>
<script type="module">
import 'https://cdn.jsdelivr.net/npm/@embedpdf/viewer@3/dist/embedpdf.js';
const welcome = '/pdfs/welcome.pdf';
const article = '/pdfs/article.pdf';
const report = '/pdfs/report.pdf';
document.getElementById('viewer').config = {
documents: [
{ src: welcome, name: 'Welcome' },
{ src: article, name: 'Engineering blog' },
{ src: report, name: 'Q3 report', active: true },
],
};
</script>Try it: the Q3 report shows first, as
activesays. 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();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.
Your feedback goes directly to the documentation team.