---
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();
```

## 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](https://www.embedpdf.com/docs/headless/react/viewing/render#take-a-snapshot)
`snapshot.scale`:

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

```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).
