---
title: "Attachments — Svelte"
description: "List the files a PDF carries, save them, let people attach and remove files, and go to a file pinned to a page."
framework: "Svelte"
source: "https://www.cloudpdf.com/docs/headless/svelte/documents/attachments"
---

# Attachments

A PDF can carry other files: a spreadsheet with the numbers behind a report, the original of a
scanned contract. They sit in two places: the document's own attachments, and file attachment
annotations, a file pinned to a spot on a page. Register `attachmentPlugin()` to list both as one
list:

**`attachment-list.svelte`**

```svelte
<script lang="ts">
  import {
    displayNameOf,
    useAttachment,
    useAttachmentState,
    type AttachedFile,
  } from '@embedpdf/svelte/attachment';
  import { saveFile } from '@embedpdf/svelte/runtime';

  const attachment = useAttachment();
  const state = useAttachmentState();

  async function save(file: AttachedFile) {
    const { bytes, name, mimeType } = await attachment.download(file);
    saveFile(bytes, name, mimeType ?? undefined);
  }
</script>

<ul>
  {#each state.files as file (file.id)}
    <li>
      <bdi>{displayNameOf(file)}</bdi>
      <button disabled={!state.canDownload} onclick={() => save(file)}>Save</button>
    </li>
  {/each}
</ul>
```

What `useAttachmentState()` gives you updates by itself: after your own changes, and on a shared
document, after someone else's. Nothing is read when a document opens: the attachments load the
first time something asks for them. The files on the pages come from the
[annotation plugin](https://www.cloudpdf.com/docs/headless/svelte/annotations), when you've registered it.

## Show the files

`files` lists the document's attachments first, in the order the document keeps them (by name),
then the files on its pages, in page order. Each file has its `name`, `mimeType`, `description`,
`size`, `checksum`, `createdAt` and `modifiedAt` (`null` when the PDF doesn't say), an `id` for
your list's keys, and its `home`:

- `{ kind: 'document', ref }`: one of the document's attachments, `ref` its name in the document.
- `{ kind: 'page', ref }`: a file attachment annotation; `ref.page` is its page.

Show a name with `displayNameOf(file)`, and put it in a `<bdi>`. A name can hide characters that
make it look like another (`invoice.pdf` that is really `invoice.exe`); `displayNameOf` takes them
out, and the `<bdi>` keeps a name in another script from turning your row around.

`status` says whether the attachments haven't been asked for yet (`'idle'`), are loading, are
there (`'ready'`), or couldn't be read.

## Save a file

`download(file)` returns the file's bytes, its name ready to save under (`displayNameOf`'s) and
its type. Hand them to `saveFile()` to save them as a browser download:

```ts
const { bytes, name, mimeType } = await attachment.download(file);
saveFile(bytes, name, mimeType ?? undefined);
```

Save a file; never open it in a new tab or a frame of your page. A file opened there runs with your
page's origin, so an HTML attachment could act as your page.

Saving takes a file out of the document, so it needs `doc.download`: check `canDownload`. A file
too large to unpack in the browser is refused (see [Large files](#large-files)).

## Attach files

`add(files)` attaches files as one change: one step to undo. A `File` the user picked brings its
name and type; bytes need a `name`:

```ts
await attachment.add([{ data: picked }, { data: bytes, name: 'figures.csv' }]);
```

Names are unique in a document: a name it has already gets a number, `notes (2).txt`, so nothing
is replaced. The files show at once, `isPending(file)` while the engine saves them, and `add()`
resolves with them. Check `canAdd` first.

## Remove a file

`remove(file)` takes a file away, as one step to undo: an undo brings back the same file, with its
bytes and dates. A file on a page goes with its annotation, through the annotation plugin.
`canRemove(file)` says whether this one may go.

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { attachmentPlugin } from '@embedpdf/svelte/attachment';
  import { cloudEngine } from '@cloudpdf/engine';
  import Files from './Files.svelte';

  import '../files.css';

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

  const welcome: OpenInput = { kind: 'grant', grant: 'shr_gvpJcG1ULUMon19C0D1wODSY' };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: welcome }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <div class="layout">
      <Files />
      <Stage class="stage">
        <RenderLayer />
      </Stage>
    </div>
  </DocumentGate>
</Viewer>
```

**`files.css`**

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

/* The files beside the pages */
.layout {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: 14px;
}

.files {
  display: flex;
  flex-direction: column;
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: 0 1px 2px var(--shadow);
  overflow: hidden;
}

.files-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 10px 10px 14px;
  border-bottom: 1px solid var(--line);
}

.files-title {
  margin: 0;
  font-size: 13px;
  font-weight: 650;
  color: var(--ink);
}

.empty {
  margin: 0;
  padding: 18px 14px;
  font-size: 12.5px;
  color: var(--muted);
}

/* One row per file: its name and size save it; a file still being added is faded */
.list {
  margin: 0;
  padding: 6px;
  list-style: none;
  overflow-y: auto;
}

.row {
  display: flex;
  align-items: center;
  border-radius: 8px;
}

.row:hover {
  background: var(--tint);
}

.row[data-pending] {
  opacity: 0.55;
}

.row-name {
  display: flex;
  flex: 1;
  flex-direction: column;
  min-width: 0;
  font: inherit;
  font-size: 13px;
  text-align: start;
  color: var(--ink);
  background: none;
  border: 0;
  padding: 6px 10px;
  cursor: pointer;
}

.row-name bdi {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.row-size {
  font-size: 11.5px;
  color: var(--muted);
}

.row-remove {
  font: inherit;
  font-size: 16px;
  line-height: 1;
  color: var(--muted);
  background: none;
  border: 0;
  border-radius: 6px;
  padding: 4px 8px;
  cursor: pointer;
  opacity: 0;
}

.row:hover .row-remove,
.row-remove:focus-visible {
  opacity: 1;
}

.row-remove:hover {
  color: var(--ink);
  background: var(--line);
}

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

.button.primary {
  color: #ffffff;
  background: var(--accent);
  border-color: var(--accent);
}

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

.button[data-disabled],
.row-name:disabled,
.row-remove:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* 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);
}
```

**`Files.svelte`**

```svelte
<!-- The files the document carries: attach some, save one, remove one. -->
<script lang="ts">
  import { saveFile } from '@embedpdf/svelte/runtime';
  import {
    displayNameOf,
    useAttachment,
    useAttachmentState,
    type AttachedFile,
  } from '@embedpdf/svelte/attachment';

  const attachment = useAttachment();
  const state = useAttachmentState();

  /** A file's size, as people read it. */
  const sizeOf = (bytes: number | null) =>
    bytes === null ? '' : bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;

  async function save(file: AttachedFile) {
    const { bytes, name, mimeType } = await attachment.download(file);
    saveFile(bytes, name, mimeType ?? undefined);
  }

  function attach(event: Event & { currentTarget: HTMLInputElement }) {
    const picked = Array.from(event.currentTarget.files ?? []);
    event.currentTarget.value = '';
    if (picked.length > 0) void attachment.add(picked.map((data) => ({ data })));
  }
</script>

<aside class="files">
  <header class="files-header">
    <h3 class="files-title">Files</h3>
    <label class="button primary" data-disabled={!state.canAdd || undefined}>
      Attach
      <input type="file" multiple hidden disabled={!state.canAdd} onchange={attach} />
    </label>
  </header>
  {#if state.files.length === 0}
    <p class="empty">Attach a file to carry it inside the document.</p>
  {:else}
    <ul class="list">
      {#each state.files as file (file.id)}
        <li class="row" data-pending={attachment.isPending(file) || undefined}>
          <button
            type="button"
            class="row-name"
            disabled={!state.canDownload}
            onclick={() => save(file)}
          >
            <bdi>{displayNameOf(file)}</bdi>
            <span class="row-size">{sizeOf(file.size)}</span>
          </button>
          <button
            type="button"
            class="row-remove"
            aria-label={`Remove ${displayNameOf(file)}`}
            disabled={!attachment.canRemove(file)}
            onclick={() => attachment.remove(file)}
          >
            ×
          </button>
        </li>
      {/each}
    </ul>
  {/if}
</aside>
```

## Go to a page's file

`reveal(file)` selects a page's file attachment annotation and brings it into view, through the
[stage](https://www.cloudpdf.com/docs/headless/svelte/viewing/stage) when you've registered it. It does nothing for one of the
document's attachments, which isn't on a page.

## Select files

`select(files)` makes files the selection, and `selected` lists them. The selection is the
reader's: show it, and act on it with your own buttons and menus.

```ts
attachment.select([file]);
await attachment.remove(selected[0]);
```

## Methods

Everything `useAttachment()` gives you.

| Method            | What it does                                                                          |
| :---------------- | :------------------------------------------------------------------------------------ |
| `get(id)`         | One file by its `id`, or `null`.                                                      |
| `isPending(file)` | Whether a change to it waits for the engine. [Attach files](#attach-files)            |
| `canRemove(file)` | Whether this file may go. [Remove a file](#remove-a-file)                             |
| `select(files)`   | Makes these files the selection; `[]` clears it. [Select files](#select-files)        |
| `add(files)`      | Attaches files as one change. Resolves with the new files.                            |
| `remove(file)`    | Takes a file away, as one change.                                                     |
| `download(file)`  | Resolves `{ bytes, name, mimeType }`, ready to save. [Save a file](#save-a-file)      |
| `reveal(file)`    | Goes to a page's file and selects it. [Go to a page's file](#go-to-a-pages-file)      |
| `ensureLoaded()`  | Reads the attachments, unless they're loaded or loading. Resolves once they're there. |
| `refresh()`       | Reads the attachments again. Rarely needed: they stay up to date by themselves.       |

Every method that returns a promise also takes a `signal` in its options, to cancel it.

## State

Everything `useAttachmentState()` gives you, as one reactive object: read its fields, and your markup updates when they change. The method in the last column reads the same value once.

| Field         | What it is                                                                                      | Read once       |
| :------------ | :---------------------------------------------------------------------------------------------- | :-------------- |
| `files`       | Every file, the document's then the pages', with your changes shown before the engine answers   | `list()`        |
| `selected`    | The selected files, in the order they were selected                                             | `getSelected()` |
| `status`      | `'idle'` until something asks for them, then `'loading'`, `'ready'`, `'forbidden'` or `'error'` | `getStatus()`   |
| `canAdd`      | Whether attaching and removing files is allowed                                                 | `canAdd()`      |
| `canDownload` | Whether saving files is allowed                                                                 | `canDownload()` |

## Events

**`attachment-changed.svelte`**

```svelte
<script lang="ts">
  import { useAttachmentEvent } from '@embedpdf/svelte/attachment';

  let notice = $state('');

  useAttachmentEvent(
    (attachment) => attachment.onChanged,
    ({ type, files, origin }) => {
      if (origin.kind === 'remote') notice = `Someone ${type} ${files.length} file(s)`;
    },
  );
</script>

<p role="status">{notice}</p>
```

| Event        | Fires when                                                                                                                           | Carries                   |
| :----------- | :----------------------------------------------------------------------------------------------------------------------------------- | :------------------------ |
| `onChanged`  | The document's attachments were added, removed or brought back by an undo, by you or by someone else                                 | `type`, `files`, `origin` |
| `onResynced` | The attachments were read from the file: the first time they're asked for, after `refresh()`, or to catch up after a lost connection | `files`                   |

`type` is `'added'`, `'removed'` or `'restored'`; for a removal, `files` are the files as they
were. A file on a page is an annotation: its changes are the annotation plugin's events.

## Permissions

| Permission               | What it allows                                  | Check                      |
| :----------------------- | :---------------------------------------------- | :------------------------- |
| `doc.open`               | Listing the files                               | none: it opened            |
| `doc.download`           | Saving a file: it leaves the document           | `attachment.canDownload()` |
| `doc.attachments.modify` | Attaching files and removing the document's own | `attachment.canAdd()`      |

Removing a file on a page is deleting its annotation, so the annotation's own permission decides:
`canRemove(file)` asks it.

## Good to know

### Large files

The server unpacks the file and applies its own limit; `download()` rejects a larger one with
`invalid-input`.

### Removed files and saving

Removing a file takes it out of the document as it is now; an incremental save still contains the
version you opened. When a file must really be gone, save with `mode: 'rewrite'`. See
[Saving](https://www.cloudpdf.com/docs/headless/svelte/documents/saving).

### The ready-made viewer

The viewer's Attachments panel is built on this plugin. See
[Panels](https://www.cloudpdf.com/docs/viewer/svelte/customize/panels).
