---
title: "Documents — Vanilla JS"
description: "Open a PDF, or several as tabs, handle passwords, and download, print or save to your server."
integration: "Vanilla JS"
source: "https://www.embedpdf.com/docs/viewer/vanilla/documents"
---

# Documents

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

```js
{ 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.html`**

```html
<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:

```js
{
  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 |

**`tabs.html`**

```html
<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 `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](customize/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:

```js
{ 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:

```js
{
  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](https://www.embedpdf.com/docs/headless/react/documents/opening):

```js
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](features/turning-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:

```js
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:

```js
{
  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.

> **Nothing to save**
>
> With CloudPDF, every annotation, comment and form value is stored on your server the moment it's
> made, and everyone who has the document open sees it. There's no file to upload and nothing to
> lose when the tab closes.
>
> [Store changes as they're made](https://www.cloudpdf.com/docs/viewer/documents)

## 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:

```js
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:

```js
{ 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](customize/theming#restyle-a-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](https://www.embedpdf.com/docs/headless/react/documents/opening#methods), [Several
documents](https://www.embedpdf.com/docs/headless/react/documents/tabs#methods) and [Saving](https://www.embedpdf.com/docs/headless/react/documents/saving#methods).
