---
title: "Opening documents — Vue"
description: "Open PDFs in the viewer, show loading states and errors, ask for passwords, and say who the user is and what they may do."
framework: "Vue"
source: "https://www.embedpdf.com/docs/headless/vue/documents/opening"
---

# Opening documents

Give the viewer the documents to open when it starts, and `<DocumentGate>` shows your content once
the document is ready:

**`initial-documents.vue`**

```vue
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import { RenderLayer } from '@embedpdf/vue/render';
import { Stage } from '@embedpdf/vue/stage';
import { engine, plugins } from './pdf';
import Spinner from './Spinner.vue';
</script>

<template>
  <Viewer
    :engine="engine"
    :plugins="plugins"
    :initial-documents="[{ source: { kind: 'url', url: '/files/contract.pdf' } }]"
  >
    <DocumentGate>
      <template #fallback><Spinner /></template>
      <Stage>
        <template #page><RenderLayer /></template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

The viewer is on screen at once; the document opens in the background. Components outside
`<DocumentGate>`, such as your toolbar's frame, render right away.

## Open a document later

`useDocuments()` opens documents while the app runs, for example one the user picked:

**`open-button.vue`**

```vue
<script setup lang="ts">
import { useDocuments } from '@embedpdf/vue/runtime';

const documents = useDocuments();

async function onPick(event: Event) {
  const file = (event.target as HTMLInputElement).files![0];
  await documents.open({ kind: 'bytes', bytes: await file.arrayBuffer() }, { name: file.name });
}
</script>

<template>
  <input type="file" accept="application/pdf" @change="onPick" />
</template>
```

`open()` resolves with `{ document }` once it's open, and the new document becomes the active one.
Pass `{ activate: false }` to open it in the background. Pick a PDF of your own here:

**`App.vue`**

```vue
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { localEngine } from '@embedpdf/engine';
import Empty from './Empty.vue';
import Toolbar from './Toolbar.vue';

import '../open.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>

<template>
  <Viewer
    :engine="engine"
    :plugins="plugins"
    :initial-documents="[{ source: ebook, name: 'ebook.pdf' }]"
  >
    <Toolbar :ebook="ebook" />
    <DocumentGate>
      <template #fallback><Empty /></template>
      <Stage class="stage">
        <template #page><RenderLayer /></template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`open.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The bar above the pages: the open document, and the buttons */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.document {
  flex: 1 1 200px;
  min-width: 0;
  font-size: 13px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.document strong {
  color: var(--ink);
  font-weight: 600;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.button:focus-within,
.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* The file input sits invisibly inside its label, which looks like a button */
.picker input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`Empty.vue`**

```vue
<!-- The gate's fallback: a document on its way, or none at all. -->
<script setup lang="ts">
import { useDocument } from '@embedpdf/vue/runtime';

const { id } = useDocument();
</script>

<template>
  <p class="loading">{{ id ? 'Opening…' : 'No document is open. Open a PDF to see it here.' }}</p>
</template>
```

**`Toolbar.vue`**

```vue
<script setup lang="ts">
import { useDocument, useDocuments } from '@embedpdf/vue/runtime';
import type { OpenSource } from '@embedpdf/vue/runtime';

defineProps<{ ebook: OpenSource }>();

const documents = useDocuments();
const { id, name, status, pageCount } = useDocument();

// A file the user picks opens next to the ebook, and becomes the active document.
async function openFile(event: Event) {
  const input = event.target as HTMLInputElement;
  const file = input.files?.[0];
  input.value = '';
  if (file) {
    await documents.open({ kind: 'bytes', bytes: await file.arrayBuffer() }, { name: file.name });
  }
}
</script>

<template>
  <div class="toolbar">
    <span class="document">
      <template v-if="id">
        <strong>{{ name ?? 'Untitled' }}</strong>
        {{ status === 'ready' ? ` · ${pageCount} pages` : ` · ${status}` }}
      </template>
      <template v-else>No document open</template>
    </span>
    <label class="button picker">
      Open a PDF…
      <input type="file" accept="application/pdf" @change="openFile" />
    </label>
    <button v-if="id" type="button" class="button" @click="documents.close(id)">Close</button>
    <button
      v-else
      type="button"
      class="button"
      @click="documents.open(ebook, { name: 'ebook.pdf' })"
    >
      Open the ebook again
    </button>
  </div>
</template>
```

## Where a document comes from

| Source                     | What it is                                                       |
| :------------------------- | :--------------------------------------------------------------- |
| `{ kind: 'url', url }`     | A file the viewer fetches. The URL can be relative to your site. |
| `{ kind: 'bytes', bytes }` | A file you already have: an upload, a file picker, a download.   |
| `async (signal) => source` | Anything else: a function that returns one of the above.         |

A function is how you add your own fetch, such as one with a login header. The viewer calls it
under the document's loading state, and `signal` cancels the download if the user closes the
document first:

```ts
const source = async (signal: AbortSignal) => {
  const response = await fetch(`/api/files/${id}`, { headers: { authorization }, signal });
  return { kind: 'bytes', bytes: await response.arrayBuffer() };
};
```

## Loading, errors and passwords

`<DocumentGate>` has something to show for each state a document can be in:

**`gate-states.vue`**

```vue
<script setup lang="ts">
import { DocumentGate } from '@embedpdf/vue/runtime';
import { RenderLayer } from '@embedpdf/vue/render';
import { Stage } from '@embedpdf/vue/stage';
import PasswordForm from './PasswordForm.vue';
import Spinner from './Spinner.vue';
</script>

<template>
  <DocumentGate>
    <template #fallback><Spinner /></template>
    <template #locked="{ document }"><PasswordForm :document="document" /></template>
    <template #error="{ document }">
      <p>Couldn't open {{ document.name }}: {{ document.error.message }}</p>
    </template>
    <Stage>
      <template #page><RenderLayer /></template>
    </Stage>
  </DocumentGate>
</template>
```

A password-protected PDF opens **locked**. Ask for the password and unlock it:

**`password-form.vue`**

```vue
<script setup lang="ts">
import { ref } from 'vue';
import { useDocuments, type DocumentInfo } from '@embedpdf/vue/runtime';
import PasswordPrompt from './PasswordPrompt.vue';

const props = defineProps<{ document: DocumentInfo }>();
const documents = useDocuments();
const wrong = ref(false);

async function submit(password: string) {
  try {
    await documents.unlock(props.document.id, { password });
  } catch {
    wrong.value = true; // it stays locked; ask again
  }
}
</script>

<template>
  <PasswordPrompt :wrong="wrong" @submit="submit" />
</template>
```

After an error, such as a network failure, `documents.retry(id)` tries again. Here the second
document isn't a PDF, so it shows its error:

**`App.vue`**

```vue
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { localEngine } from '@embedpdf/engine';
import OpenError from './OpenError.vue';
import PasswordForm from './PasswordForm.vue';
import Tabs from './Tabs.vue';

import '../states.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// A file that isn't a PDF, so its document can't be opened.
const broken: OpenInput = { kind: 'bytes', bytes: new TextEncoder().encode('Not a PDF') };
</script>

<template>
  <Viewer
    :engine="engine"
    :plugins="plugins"
    :initial-documents="[
      { source: ebook, name: 'ebook.pdf' },
      { source: broken, name: 'broken.pdf', active: true },
    ]"
  >
    <Tabs />
    <DocumentGate>
      <template #fallback><div class="panel">Opening…</div></template>
      <template #locked="{ document }"><PasswordForm :document="document" /></template>
      <template #error="{ document }"><OpenError :document="document" /></template>
      <Stage class="stage">
        <template #page><RenderLayer /></template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`states.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --focus: #7db6ff;
  --danger: #b42318;
  --danger-tint: #fef3f2;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The tabs: one per document, with its status */
.tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 14px;
}

.tab {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  color: var(--muted);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 12px;
  cursor: pointer;
  box-shadow: 0 1px 2px var(--shadow);
}

.tab[aria-selected='true'] {
  color: var(--ink);
  background: var(--tint);
  border-color: var(--focus);
}

.tab:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* The dot before a tab's name: its status */
.status {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--line-strong);
}

.status[data-status='ready'] {
  background: #12b76a;
}

.status[data-status='loading'] {
  background: var(--focus);
}

.status[data-status='locked'] {
  background: #f79009;
}

.status[data-status='error'] {
  background: var(--danger);
}

/* What the gate shows instead of the pages */
.panel {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  height: 420px;
  box-sizing: border-box;
  padding: 24px;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
}

.panel h3 {
  margin: 0;
  font-size: 15px;
  color: var(--ink);
}

.panel p {
  margin: 0;
  max-width: 360px;
}

.panel[data-tone='error'] {
  background: var(--danger-tint);
}

.panel[data-tone='error'] p {
  color: var(--danger);
}

.actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
}

.field {
  appearance: none;
  min-width: 0;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  box-shadow: 0 1px 2px var(--shadow);
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible,
.field:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}
```

**`OpenError.vue`**

```vue
<script setup lang="ts">
import { useDocuments } from '@embedpdf/vue/runtime';
import type { FailedDocumentInfo } from '@embedpdf/vue/runtime';

defineProps<{ document: FailedDocumentInfo }>();

const documents = useDocuments();
</script>

<template>
  <div class="panel" data-tone="error">
    <h3>Couldn’t open {{ document.name }}</h3>
    <p>{{ document.error.message }}</p>
    <div class="actions">
      <button type="button" class="button" @click="documents.retry(document.id)">Try again</button>
      <button type="button" class="button" @click="documents.close(document.id)">Close</button>
    </div>
  </div>
</template>
```

**`PasswordForm.vue`**

```vue
<script setup lang="ts">
import { ref } from 'vue';
import { useDocuments } from '@embedpdf/vue/runtime';
import type { DocumentInfo } from '@embedpdf/vue/runtime';

const props = defineProps<{ document: DocumentInfo }>();

const documents = useDocuments();
const password = ref('');
const wrong = ref(props.document.passwordProvided ?? false);

async function unlock() {
  try {
    await documents.unlock(props.document.id, { password: password.value });
  } catch {
    wrong.value = true; // it stays locked; ask again
  }
}
</script>

<template>
  <div class="panel">
    <h3>{{ document.name }} needs a password</h3>
    <p v-if="wrong">That password isn’t right. Try again.</p>
    <div class="actions">
      <input v-model="password" class="field" type="password" aria-label="Password" />
      <button type="button" class="button" @click="unlock">Unlock</button>
    </div>
  </div>
</template>
```

**`Tabs.vue`**

```vue
<script setup lang="ts">
import { useDocuments, useDocumentsState } from '@embedpdf/vue/runtime';

const documents = useDocuments();
const { documents: open, activeId } = useDocumentsState();
</script>

<template>
  <div class="tabs" role="tablist">
    <button
      v-for="document in open"
      :key="document.id"
      type="button"
      role="tab"
      class="tab"
      :aria-selected="document.id === activeId"
      @click="documents.setActive(document.id)"
    >
      <span class="status" :data-status="document.status" />
      {{ document.name }}
    </button>
  </div>
</template>
```

To show the state yourself, for example on a tab, `useDocument()` gives you the document's
`status`: `'loading'`, `'locked'`, `'ready'` or `'error'`. Every field is listed under
[State](#state).

## Who the user is and what they may do

Tell the viewer who the user is and what they may do, once. Every document it opens uses them:

**`identity-scope.vue`**

```vue
<script setup lang="ts">
import { Viewer } from '@embedpdf/vue/runtime';
import { engine, plugins } from './pdf';

const identity = { userId: 'u_381', displayName: 'Dana Smith' };
const scope = ['doc.open', 'doc.render', 'doc.text.select', 'doc.annotate.read', 'annotations:create:self'];
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :identity="identity" :scope="scope" />
</template>
```

- **`identity`** is who the user is. Their `displayName` becomes the author of the annotations they
  make.
- **`scope`** is what they may do, as a list of permissions. Leave it out and they may do what
  the file allows: everything, unless the PDF restricts itself, as in Acrobat.

**`App.vue`**

```vue
<script setup lang="ts">
import { ref } from 'vue';
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { localEngine } from '@embedpdf/engine';
import { roles, type Role } from './roles';
import Toolbar from './Toolbar.vue';

import '../identity.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};

const dana = { userId: 'u_381', displayName: 'Dana Smith' };
const role = ref<Role>('reader');
</script>

<template>
  <Viewer
    :engine="engine"
    :plugins="plugins"
    :identity="dana"
    :scope="roles[role]"
    :initial-documents="[{ source: ebook, name: 'ebook.pdf' }]"
  >
    <Toolbar v-model:role="role" :ebook="ebook" />
    <DocumentGate>
      <template #fallback><p class="loading">Opening…</p></template>
      <Stage class="stage">
        <template #page><RenderLayer /></template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`identity.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --focus: #7db6ff;
  --allowed: #067647;
  --allowed-tint: #ecfdf3;
  --refused: #b42318;
  --refused-tint: #fef3f2;
  --shadow: rgb(7 32 76 / 0.04);
}

/* Who is signed in, and what the open document allows */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
  font-size: 13px;
  color: var(--ink);
}

.label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-right: auto;
}

.select {
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 10px;
}

/* A check's answer: green when the document allows it, red when it doesn't */
.check {
  font-size: 12px;
  font-weight: 600;
  border-radius: 999px;
  padding: 4px 10px;
}

.check[data-allowed='true'] {
  color: var(--allowed);
  background: var(--allowed-tint);
}

.check[data-allowed='false'] {
  color: var(--refused);
  background: var(--refused-tint);
}

.note {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  min-height: 34px;
  margin: 0 0 14px;
  font-size: 13px;
  color: var(--muted);
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 12px;
  cursor: pointer;
  box-shadow: 0 1px 2px var(--shadow);
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible,
.select:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* The pages */
.stage {
  height: 400px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`roles.ts`**

```typescript
// What each role may do, as permissions.
export const roles = {
  reader: ['doc.open', 'doc.render', 'doc.text.select'],
  editor: [
    'doc.open',
    'doc.render',
    'doc.text.select',
    'doc.text.copy',
    'doc.download',
    'doc.print',
  ],
};

export type Role = keyof typeof roles;
```

**`Toolbar.vue`**

```vue
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useDocument, useDocuments } from '@embedpdf/vue/runtime';
import type { OpenSource } from '@embedpdf/vue/runtime';
import type { Role } from './roles';

const props = defineProps<{ ebook: OpenSource }>();
const role = defineModel<Role>('role', { required: true });

const documents = useDocuments();
const { id } = useDocument();
// The role the open document was opened with: it keeps that one.
const openedAs = ref(role.value);

// The checks belong to the open document: read them again whenever another one opens.
const canDownload = computed(() => id.value !== '' && documents.canDownload());
const canPrint = computed(() => id.value !== '' && documents.canPrint());

async function openAgain() {
  await documents.close(id.value);
  await documents.open(props.ebook, { name: 'ebook.pdf' });
  openedAs.value = role.value;
}
</script>

<template>
  <div class="toolbar">
    <label class="label">
      Dana Smith, as
      <select v-model="role" class="select">
        <option value="reader">reader</option>
        <option value="editor">editor</option>
      </select>
    </label>
    <span class="check" :data-allowed="canDownload">Download</span>
    <span class="check" :data-allowed="canPrint">Print</span>
  </div>
  <p class="note">
    This document opened for a {{ openedAs }}.
    <button v-if="openedAs !== role" type="button" class="button" @click="openAgain">
      Open it again as {{ role }}
    </button>
  </p>
</template>
```

Change them on the viewer, and the documents you open after
that use the new values; documents already open keep theirs.

A document can have its own, which replace the viewer's for that document:

```ts
const initialDocuments = [{ source, scope: ['doc.open', 'doc.render'] }]; // this one is read-only
await documents.open(source, { scope: ['doc.open', 'doc.render'] }); // and so is this one
```

[Permissions](../concepts/permissions) lists every permission.

## Close a document

```ts
await documents.close(document.id);
```

Closing frees the document's memory in the engine. The next document becomes the active one.

## Methods

What `useDocuments()` gives you for opening and closing documents and reading their pages. Tabs and
the active document are on [Several documents](tabs#methods), downloading and printing on
[Saving & downloading](saving#methods).

| Method                     | What it does                                                                                                                                                                                                                  |
| :------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `open(source, options?)`   | Opens a document and makes it the active one. Resolves `{ document }` once it's ready, or locked with a password; rejects when it can't be opened. [Open a document later](#open-a-document-later)                            |
| `canOpenFiles()`           | Whether `open()` takes a file's bytes, such as a file the user picked, or a URL it downloads: `true` on the local engine, `false` on CloudPDF, where every document comes from your server                                    |
| `openAll(documents)`       | Opens several documents, as `initialDocuments` does: every tab appears at once, in order, and the one marked `active` (or the first) becomes the active one. A failure shows in that document's `status` instead of throwing. |
| `unlock(id, { password })` | Unlocks a locked document. Rejects on a wrong password, and the document stays locked so you can ask again. [Loading, errors and passwords](#loading-errors-and-passwords)                                                    |
| `retry(id)`                | Tries a failed document again, with the same source and options. Resolves like `open()`.                                                                                                                                      |
| `close(id)`, `closeAll()`  | Closes one document, or every document, and frees its memory. [Close a document](#close-a-document)                                                                                                                           |
| `get(id?)`                 | A document's fields, the ones [`useDocument()`](#state) gives you, or `null` when it isn't open. Without an `id`, the document in scope                                                                                       |
| `listPages(id?)`           | The document's pages, in order. In a component, use [`usePageList()`](../viewing/page-view#pick-the-page).                                                                                                                    |
| `getPage(page, id?)`       | One page, by its `ref` or its index, or `null` when the document doesn't have it                                                                                                                                              |
| `getPageIndex(ref, id?)`   | A page's position, from 0, or `-1` when the document doesn't have it                                                                                                                                                          |
| `getRevision(id?)`         | A number that goes up each time pages are added, removed, moved or rotated; `-1` with no document                                                                                                                             |

`options` for `open()` are `name` (what its tab shows), `activate: false` (open it in the
background), `password`, and, on the local engine, `scope` and `identity`. Without an `id`, the page
methods use the document in scope: the one a [`<DocumentScope>`](tabs#talk-to-a-specific-document)
around your component names, or else the active one. To stop an open that's still loading, close the
document: that cancels its download too.

## State

`useDocument()` gives you the document a component talks to: the active one, or the one its
`<DocumentScope>` names. Its fields are refs: your template updates when one changes. Outside a component,
`get(id)` gives the same fields. With no document open yet, it reads as one still on its way: an
empty `id` and `status: 'loading'`, so a component that shows it never has to check for `null`.

| Field               | What it is                                                                                                              |
| :------------------ | :---------------------------------------------------------------------------------------------------------------------- |
| `id`                | The document's id, for `close(id)`, `unlock(id, …)` and `<DocumentScope>`                                               |
| `name`              | The name its tab shows, from `open()` or `rename()`                                                                     |
| `status`            | `'loading'`, `'locked'`, `'ready'` or `'error'`                                                                         |
| `pageCount`         | How many pages it has; `0` until it's ready                                                                             |
| `hasUnsavedChanges` | Whether it has changes that weren't downloaded yet. [Warn before changes are lost](saving#warn-before-changes-are-lost) |
| `passwordProvided`  | When it's locked: whether a password was tried and was wrong                                                            |
| `error`             | Why it couldn't be opened, when `status` is `'error'`: `{ code, message }`                                              |

Every open document, with the same fields, comes from [`useDocumentsState()`](tabs#state).

## Events

**`open-failed.vue`**

```vue
<script setup lang="ts">
import { ref } from 'vue';
import { useDocumentsEvent } from '@embedpdf/vue/runtime';

const message = ref('');

useDocumentsEvent(
  (documents) => documents.onOpenFailed,
  ({ error }) => (message.value = `Couldn't open the file: ${error.message}`),
);
</script>

<template>
  <p role="alert">{{ message }}</p>
</template>
```

| Event            | Fires when                                  | Carries                           |
| :--------------- | :------------------------------------------ | :-------------------------------- |
| `onOpened`       | A document is open and ready                | `documentId`, `info`              |
| `onOpenFailed`   | A document couldn't be opened               | `documentId`, `error`             |
| `onLocked`       | A document needs a password                 | `documentId`, `passwordProvided`  |
| `onClosed`       | A document was closed                       | `documentId`                      |
| `onPagesChanged` | Pages were added, removed, moved or rotated | `documentId`, `pages`, `revision` |

Switching the active document is on [Several documents](tabs#events).

## Good to know

### Opening large files

The local engine needs the whole file before it can show the first page, so a large PDF on a slow
connection takes as long as its download.

> **Open a 500 MB PDF in a second**
>
> With CloudPDF, the file stays on your server. The viewer asks only for the pages on screen, as
> images, so the first page shows at once whatever the file's size, and a user who may not download
> the file never receives it.
>
> [Open documents with CloudPDF](https://www.cloudpdf.com/docs/headless/vue/documents/opening)

### `initialDocuments` is read once

Like `engine` and `plugins`, the viewer reads `initialDocuments` when it mounts. To open another
document later, use `documents.open()`.

### Several documents

A viewer can hold several documents at once, like tabs. See [Several documents](tabs).
