---
title: "Annotations in code — Vue"
description: "Read, create, change, reorder and delete annotations from your own code, with changes shown at once."
framework: "Vue"
source: "https://www.embedpdf.com/docs/headless/vue/annotations/with-code"
---

# Annotations in code

Everything people do with the pointer, your code can do too. `useAnnotation()` gives you the
annotations on screen, and every change you make shows at once, before the engine has saved it:

```ts
const { annotation: square } = await annotation.create(page.ref, {
  subtype: 'square',
  box: { x: 72, y: 92, width: 200, height: 100 },
  color: '#0078ff',
});

await annotation.update(square.ref, { contents: 'Check this table' });
await annotation.delete(square.ref);
```

**`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 { interactionPlugin } from '@embedpdf/vue/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/vue/annotation';
import { localEngine } from '@embedpdf/engine';
import SquareControls from './SquareControls.vue';

import '../create-update.css';

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

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

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <SquareControls />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <AnnotationLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`create-update.css`**

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

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

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  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:active {
  transform: translateY(1px);
}

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

.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--line-strong);
}

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

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

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

**`SquareControls.vue`**

```vue
<!-- Create a rectangle, then change, move and delete it, all from code. -->
<script setup lang="ts">
import { computed, shallowRef, watch } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import { useAnnotation, useAnnotationList, useAnnotationState } from '@embedpdf/vue/annotation';
import type { AnnotationRef } from '@embedpdf/vue/annotation';

const annotation = useAnnotation();
const ready = useAnnotationState((state) => state.status === 'ready');
const pages = usePageList();
const cover = computed(() => pages.value[0]?.ref);
const squareRef = shallowRef<AnnotationRef | null>(null);
const annotations = useAnnotationList(); // follows the annotations as they change, for the reads below

const square = computed(() => {
  void annotations.value; // read again whenever the annotations change
  return squareRef.value ? annotation.get(squareRef.value) : null;
});
const canUpdate = computed(() => square.value !== null && annotation.canUpdate(square.value.ref));
const canDelete = computed(() => square.value !== null && annotation.canDelete(square.value.ref));
const status = computed(() => {
  if (!square.value) return 'No rectangle';
  return annotation.isPending(square.value.ref) ? 'Saving…' : 'Saved';
});

async function add() {
  if (!cover.value) return;
  const { annotation: made } = await annotation.create(cover.value, {
    subtype: 'square',
    box: { x: 72, y: 592, width: 200, height: 100 },
    color: '#0078ff',
    strokeWidth: 3,
  });
  squareRef.value = made.ref;
}

// On load: one rectangle, to change.
let started = false;
watch(
  ready,
  (isReady) => {
    if (!isReady || started) return;
    started = true;
    void add();
  },
  { immediate: true },
);

function makeRed() {
  if (square.value) void annotation.update(square.value.ref, { color: '#dc143c' });
}

function moveRight() {
  if (!square.value) return;
  const { rect } = square.value;
  void annotation.update(square.value.ref, { rect: { ...rect, x: rect.x + 40 } }); // 40 points right
}

function turn() {
  if (square.value) void annotation.update(square.value.ref, { rotation: 90 });
}

function remove() {
  if (!square.value) return;
  void annotation.delete(square.value.ref);
  squareRef.value = null;
}
</script>

<template>
  <div class="toolbar">
    <button type="button" class="button" :disabled="square !== null" @click="add">Create</button>
    <button type="button" class="button" :disabled="!canUpdate" @click="makeRed">
      Make it red
    </button>
    <button type="button" class="button" :disabled="!canUpdate" @click="moveRight">
      Move right
    </button>
    <button type="button" class="button" :disabled="!canUpdate" @click="turn">Turn to 90°</button>
    <button type="button" class="button" :disabled="!canDelete" @click="remove">Delete</button>
    <span class="spacer" />
    <output class="readout">{{ status }}</output>
  </div>
</template>
```

> **Try it:** each button is one call. **Saving…** shows while the engine writes a change.

An annotation here is the engine's record: the same kinds, fields and names as the
[engine's annotation pages](https://www.embedpdf.com/docs/engine/annotations). Each kind's page there lists its fields.
These pages cover what the plugin adds on top.

## Read annotations

```ts
annotation.list(); // every annotation, in drawing order
annotation.list({ pages: [page.ref] }); // one page
annotation.list({ subtype: 'highlight' }); // one kind
annotation.get(ref); // one, or null
annotation.getAt(page.ref, { x: 120, y: 80 }); // the topmost one at a point, or null
```

To keep a component up to date, use `useAnnotationList()`. Your component updates
when the list changes:

**`list.vue`**

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

const highlights = useAnnotationList({ subtype: 'highlight' });
</script>

<template>
  <p>{{ highlights.length }} highlights</p>
</template>
```

**`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 { interactionPlugin } from '@embedpdf/vue/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/vue/annotation';
import { localEngine } from '@embedpdf/engine';
import AddAnnotations from './AddAnnotations.vue';
import AnnotationList from './AnnotationList.vue';

import '../list.css';

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

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

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <AddAnnotations />
      <div class="viewer">
        <Stage class="stage">
          <template #page>
            <RenderLayer />
            <AnnotationLayer />
          </template>
        </Stage>
        <AnnotationList />
      </div>
    </DocumentGate>
  </Viewer>
</template>
```

**`list.css`**

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

/* One choice of a few: the pressed button is the current one */
.segmented {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 2px;
  padding: 3px;
  background: var(--tint);
  border: 1px solid var(--line);
  border-radius: 11px;
}

.segmented button {
  font: inherit;
  font-size: 12.5px;
  font-weight: 550;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 8px;
  padding: 6px 12px;
  cursor: pointer;
  white-space: nowrap;
}

.segmented button:hover {
  color: var(--ink);
}

.segmented button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--surface);
  box-shadow: 0 1px 2px rgb(7 32 76 / 0.1);
}

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

/* The list: a kind filter, then one row per annotation */
.panel .segmented {
  margin-bottom: 8px;
}

.count {
  margin: 0 0 6px;
  font-size: 12px;
  color: var(--muted);
}

.items {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 100%;
  font: inherit;
  font-size: 13px;
  text-align: left;
  color: var(--text);
  background: transparent;
  border: 0;
  border-radius: 9px;
  padding: 8px 10px;
  cursor: pointer;
}

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

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

.kind {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The pages and the panel: stacked, and side by side where there's room */
.viewer {
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.stage,
.panel {
  flex: 1 1 100%;
  box-sizing: border-box;
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.stage {
  height: 340px;
  background: var(--canvas);
}

.panel {
  max-height: 260px;
  overflow-y: auto;
  margin: 0;
  padding: 12px;
  background: var(--surface);
}

@container (min-width: 620px) {
  .stage {
    flex: 1 1 0;
    min-width: 0;
    height: 420px;
  }

  .panel {
    flex: 0 0 260px;
    max-height: 420px;
  }
}

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

**`AddAnnotations.vue`**

```vue
<!-- On load: highlights, notes and a rectangle on the first two pages. -->
<script setup lang="ts">
import { watch } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import { useAnnotation, useAnnotationState } from '@embedpdf/vue/annotation';

// A line of text, as the four corners a highlight takes.
const quad = (x: number, y: number, width: number, height: number) => ({
  upperLeft: { x, y },
  upperRight: { x: x + width, y },
  lowerLeft: { x, y: y + height },
  lowerRight: { x: x + width, y: y + height },
});

const annotation = useAnnotation();
const ready = useAnnotationState((state) => state.status === 'ready');
const pages = usePageList();

let added = false;
watch(
  [ready, pages],
  ([isReady, [cover, second]]) => {
    if (!isReady || !cover || !second || added) return;
    added = true;
    void annotation.create(cover.ref, {
      subtype: 'highlight',
      quadPoints: [quad(106, 218, 352, 49)],
      color: '#ffcd45',
      contents: 'The title',
    });
    void annotation.create(cover.ref, {
      subtype: 'text',
      rect: { x: 470, y: 232, width: 20, height: 20 },
      contents: 'Can we shorten it?',
      color: '#facc15',
    });
    void annotation.create(second.ref, {
      subtype: 'highlight',
      quadPoints: [quad(57, 57, 242, 36), quad(57, 93, 265, 36)],
      color: '#ffcd45',
      contents: 'The opening line',
    });
    void annotation.create(second.ref, {
      subtype: 'square',
      box: { x: 52, y: 580, width: 470, height: 64 },
      color: '#e5484d',
      strokeWidth: 2,
      contents: 'Rewrite this paragraph',
    });
  },
  { immediate: true },
);
</script>

<template></template>
```

**`AnnotationList.vue`**

```vue
<!-- The annotations of one kind, in drawing order. A click shows one on its page. -->
<script setup lang="ts">
import { shallowRef } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import { useStage } from '@embedpdf/vue/stage';
import { annotationKey, useAnnotation, useAnnotationList } from '@embedpdf/vue/annotation';
import type { Annotation, AnnotationSubtype } from '@embedpdf/vue/annotation';

const KINDS: { label: string; subtype?: AnnotationSubtype }[] = [
  { label: 'All' },
  { label: 'Highlights', subtype: 'highlight' },
  { label: 'Notes', subtype: 'text' },
  { label: 'Rectangles', subtype: 'square' },
];

const kind = shallowRef(KINDS[0]!);
const annotations = useAnnotationList(() =>
  kind.value.subtype ? { subtype: kind.value.subtype } : undefined,
);
const pages = usePageList();
const stage = useStage();
const annotation = useAnnotation();

const pageNumberOf = (objectNumber: number) =>
  pages.value.findIndex((page) => page.ref.objectNumber === objectNumber) + 1;

// What it is, and where: "highlight · page 2".
const kindOf = (each: Annotation) =>
  `${each.subtype} · page ${pageNumberOf(each.page.objectNumber)}`;

function show(each: Annotation) {
  stage.reveal(each.page, { rect: each.rect });
  annotation.selection.set([each.ref]);
}
</script>

<template>
  <div class="panel">
    <div class="segmented" role="group" aria-label="Kind">
      <button
        v-for="each in KINDS"
        :key="each.label"
        type="button"
        :aria-pressed="each === kind"
        @click="kind = each"
      >
        {{ each.label }}
      </button>
    </div>
    <p class="count">{{ annotations.length }} found</p>
    <ul class="items">
      <li v-for="each in annotations" :key="annotationKey(each.ref)">
        <button type="button" class="item" @click="show(each)">
          <span class="kind">{{ kindOf(each) }}</span>
          <span>{{ each.contents ?? '—' }}</span>
        </button>
      </li>
    </ul>
  </div>
</template>
```

> **Try it:** pick a kind, then click a row to go to it.

Each annotation has a `subtype` (its kind), a `rect` (where it is on its page), a `page` and a
`ref`. Keep the `ref`: it's how you change or delete the annotation later. Positions are in
points from the top-left of the page; see [Coordinates](../concepts/coordinates).

Annotations are loaded in the background when a document opens. `useAnnotationState()` has a
`status`: `'loading'` until they're all in, then `'ready'`.

## Create an annotation

`create()` takes the page, then the annotation's kind and fields:

```ts
const { annotation: note } = await annotation.create(page.ref, {
  subtype: 'text',
  rect: { x: 500, y: 72, width: 20, height: 20 },
  contents: 'Is this total right?',
});
```

Fields you leave out get the kind's default. For example, a new annotation prints with the page
unless you pass `print: false`. Every kind the engine can create, the plugin can too; the
[engine's pages](https://www.embedpdf.com/docs/engine/annotations) show each one.

Some options change how the plugin treats the new annotation:

```ts
await annotation.create(page.ref, { subtype: 'square', box }, undefined, {
  tool: 'square',
  select: true,
});
```

| Option   | What it does                                                                                   |
| :------- | :--------------------------------------------------------------------------------------------- |
| `tool`   | Starts from that tool's defaults, the user's current drawing style, before your own fields.    |
| `select` | Selects the new annotation, as drawing it would. Without it, code never changes the selection. |

Stamps and file attachments carry bytes. They go in the third argument, `resources`, named for what
they are: a stamp's `appearance`, an attachment's `file`.

```ts
const image = await (await fetch('/approved.png')).blob();
await annotation.create(page.ref, { subtype: 'stamp', box }, { appearance: image });

// A file pinned to the page, drawn as an icon, 20 × 20 like a note
await annotation.create(
  page.ref,
  { subtype: 'file-attachment', rect: { x: 40, y: 60, width: 20, height: 20 } },
  { file: pickedFile }, // a File brings its name and type
);
```

For stamps from a library, see [Stamps](stamps).

### From the selected text

`createFromSelection()` turns the text a person selected into a highlight, an underline, a
strikeout or a squiggly line, one per page:

```ts
const { annotations } = await annotation.createFromSelection('highlight');
```

It takes a tool id, so your own markup tools work too. The text selection is cleared afterwards;
pass `{ clear: false }` to keep it.

**`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 { interactionPlugin } from '@embedpdf/vue/interaction';
import { SelectionLayer, selectionPlugin } from '@embedpdf/vue/selection';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/vue/annotation';
import { localEngine } from '@embedpdf/engine';
import MarkupToolbar from './MarkupToolbar.vue';

import '../from-selection.css';

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

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

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <MarkupToolbar />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <SelectionLayer />
          <AnnotationLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`from-selection.css`**

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

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

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  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:active {
  transform: translateY(1px);
}

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

.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--line-strong);
}

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

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

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

**`MarkupToolbar.vue`**

```vue
<!-- The selected text becomes a mark, one per page; the selection is cleared afterwards. -->
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import { useSelection, useSelectionState } from '@embedpdf/vue/selection';
import { useAnnotation } from '@embedpdf/vue/annotation';

// On the cover: the characters of the title's first line.
const FIRST_LINE = { start: 10, count: 17 };

const MARKUP = [
  { tool: 'highlight', label: 'Highlight' },
  { tool: 'underline', label: 'Underline' },
  { tool: 'strikeout', label: 'Strike out' },
  { tool: 'squiggly', label: 'Squiggly' },
];

const annotation = useAnnotation();
const selection = useSelection();
const { hasSelection } = useSelectionState();
const pages = usePageList();
const cover = computed(() => pages.value[0]?.ref);
const status = ref('');

// On load: the title's first line is selected, ready to mark.
watch(
  cover,
  (page) => {
    if (page) selection.select({ page, ...FIRST_LINE });
  },
  { immediate: true },
);

async function mark(tool: string, label: string) {
  const { annotations } = await annotation.createFromSelection(tool);
  status.value = `${label}: ${annotations.length} made`;
}
</script>

<template>
  <div class="toolbar">
    <button
      v-for="{ tool, label } in MARKUP"
      :key="tool"
      type="button"
      class="button"
      :disabled="!hasSelection"
      @click="mark(tool, label)"
    >
      {{ label }}
    </button>
    <span class="spacer" />
    <output class="readout">{{ hasSelection ? 'Text selected' : status || 'Select text' }}</output>
  </div>
</template>
```

> **Try it:** the title's first line is selected: press **Highlight**. Then select other words
> with the pointer and mark them.

## Change an annotation

`update()` takes the `ref` and only the fields you want to change:

```ts
await annotation.update(ref, { color: '#dc143c', contents: 'Reviewed' });
```

A field you leave out keeps its value, and `null` removes it (`{ interiorColor: null }` for no
fill). You can also pass an annotation you read straight back, which makes "change a copy, then
save it" simple:

```ts
const square = annotation.get(ref);
if (square?.subtype === 'square') await annotation.update(ref, { ...square, opacity: 0.5 });
```

To move or resize any annotation, send a new `rect`:

```ts
const { rect } = annotation.get(ref)!;
await annotation.update(ref, { rect: { ...rect, x: rect.x + 40 } }); // 40 points right
```

For a shape drawn from points, like a line or ink, the engine moves every point for you. The rules
for resizing are on the engine's [Annotations](https://www.embedpdf.com/docs/engine/annotations#move-or-resize-by-rect)
page.

Rotation is a field too: `update(ref, { rotation: 90 })` turns a shape or a stamp to 90° clockwise.

## Delete an annotation

```ts
await annotation.delete(ref);
```

Deleting an annotation also deletes its popup and its replies, as other PDF apps do.

## Change the drawing order

Annotations on a page are drawn in order, and the last one is on top. `reorder()` moves
annotations together, in the order you pass them, next to another annotation of the page or to the
back or the front:

```ts
await annotation.reorder([ref], 'start'); // to the back
await annotation.reorder([ref], 'end'); // to the front
await annotation.reorder([ref], { after: other }); // just above another annotation
```

Form fields are drawn above every annotation and keep their own order, so `reorder()` never
places an annotation above a field.

**`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 { interactionPlugin } from '@embedpdf/vue/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/vue/annotation';
import { localEngine } from '@embedpdf/engine';
import AddRectangles from './AddRectangles.vue';
import OrderControls from './OrderControls.vue';

import '../order.css';

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

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

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <AddRectangles />
      <OrderControls />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <AnnotationLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`order.css`**

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

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

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  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:active {
  transform: translateY(1px);
}

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

.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--line-strong);
}

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

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

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

**`AddRectangles.vue`**

```vue
<!-- On load: three filled rectangles stacked on the cover, the middle one selected. -->
<script setup lang="ts">
import { computed, watch } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import { useAnnotation, useAnnotationState } from '@embedpdf/vue/annotation';

const COLORS = [
  { color: '#e5484d', fill: '#ffd1d3' },
  { color: '#30a46c', fill: '#c9f0da' },
  { color: '#1e90ff', fill: '#cfe6ff' },
];

const annotation = useAnnotation();
const ready = useAnnotationState((state) => state.status === 'ready');
const pages = usePageList();
const cover = computed(() => pages.value[0]?.ref);

let added = false;
watch(
  [ready, cover],
  ([isReady, page]) => {
    if (!isReady || !page || added) return;
    added = true;
    COLORS.forEach(({ color, fill }, index) => {
      void annotation.create(
        page,
        {
          subtype: 'square',
          box: { x: 300 + index * 50, y: 520 + index * 30, width: 160, height: 90 },
          color,
          interiorColor: fill,
          strokeWidth: 3,
        },
        undefined,
        { select: index === 1 },
      );
    });
  },
  { immediate: true },
);
</script>

<template></template>
```

**`OrderControls.vue`**

```vue
<!-- The last annotation on a page is drawn on top. -->
<script setup lang="ts">
import { computed } from 'vue';
import {
  annotationKey,
  useAnnotation,
  useAnnotationList,
  useAnnotationState,
} from '@embedpdf/vue/annotation';

const annotation = useAnnotation();
const { selected } = useAnnotationState();
const first = computed(() => selected.value[0]);
const onPage = useAnnotationList(() => (first.value ? { pages: [first.value.page] } : undefined));
const position = computed(() => {
  const wanted = first.value;
  if (!wanted) return -1;
  return onPage.value.findIndex((each) => annotationKey(each.ref) === annotationKey(wanted.ref));
});

function sendToBack() {
  if (first.value) void annotation.reorder([first.value.ref], 'start');
}

function bringToFront() {
  if (first.value) void annotation.reorder([first.value.ref], 'end');
}
</script>

<template>
  <div class="toolbar">
    <button
      type="button"
      class="button"
      :disabled="!first || selected.length !== 1"
      @click="sendToBack"
    >
      Send to back
    </button>
    <button
      type="button"
      class="button"
      :disabled="!first || selected.length !== 1"
      @click="bringToFront"
    >
      Bring to front
    </button>
    <span class="spacer" />
    <output class="readout">
      {{
        position >= 0 ? `${position + 1} of ${onPage.length}, from the back` : 'Select a rectangle'
      }}
    </output>
  </div>
</template>
```

> **Try it:** send the green rectangle to the back, then select another and bring it to the
> front.

## Changes show at once

A change shows the moment you make it. The engine saves it in the background, and your `await`
resolves once it has. Until then the annotation is **pending**:

```ts
annotation.isPending(ref); // true while the engine is still saving a change to it
```

You rarely need this: `get()` and `list()` already show the change. It's useful for a small
"saving…" hint.

If the engine refuses a change (a lock set by someone else, or permission taken away), the change
disappears again, and an event says why:

**`write-failed.vue`**

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

import { toast } from './toast';

useAnnotationEvent(
  (annotation) => annotation.onWriteFailed,
  ({ error }) => toast(`Couldn't save: ${error.message}`),
);
</script>
```

The `await` of the call that made the change rejects with the same error.

## Copy an annotation

A read passed to `create()` makes the same annotation again, on another page or in another
document:

```ts
const copy = annotation.get(ref)!;

await annotation.create(otherPage.ref, copy);
```

**`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 { interactionPlugin } from '@embedpdf/vue/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/vue/annotation';
import { localEngine } from '@embedpdf/engine';
import AddAnnotations from './AddAnnotations.vue';
import CopyButton from './CopyButton.vue';

import '../copy.css';

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

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

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <AddAnnotations />
      <CopyButton />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <AnnotationLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`copy.css`**

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

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

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  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:active {
  transform: translateY(1px);
}

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

.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--line-strong);
}

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

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

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

**`AddAnnotations.vue`**

```vue
<!-- On load: a rectangle and a text box on the cover, the rectangle selected and in view. -->
<script setup lang="ts">
import { computed, watch } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import { useStage } from '@embedpdf/vue/stage';
import { useAnnotation, useAnnotationState } from '@embedpdf/vue/annotation';

const annotation = useAnnotation();
const stage = useStage();
const ready = useAnnotationState((state) => state.status === 'ready');
const pages = usePageList();
const cover = computed(() => pages.value[0]?.ref);

let added = false;
watch(
  [ready, cover],
  ([isReady, page]) => {
    if (!isReady || !page || added) return;
    added = true;
    void annotation.create(page, {
      subtype: 'free-text',
      box: { x: 300, y: 512, width: 230, height: 40 },
      contents: 'Copy me too',
      fontSize: 16,
      fontColor: '#1a2748',
      interiorColor: '#fffbe6',
    });
    void annotation
      .create(
        page,
        {
          subtype: 'square',
          box: { x: 96, y: 506, width: 178, height: 54 },
          color: '#e5484d',
          interiorColor: '#ffe4e1',
          strokeWidth: 3,
        },
        undefined,
        { select: true },
      )
      .then(({ annotation: made }) => stage.reveal(page, { rect: made.rect }));
  },
  { immediate: true },
);
</script>

<template></template>
```

**`CopyButton.vue`**

```vue
<!-- A read passed to create() makes the same annotation again, here on the next page. -->
<script setup lang="ts">
import { computed, ref } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import { useStage } from '@embedpdf/vue/stage';
import { useAnnotation, useAnnotationState } from '@embedpdf/vue/annotation';

const annotation = useAnnotation();
const stage = useStage();
const pages = usePageList();
const { selected } = useAnnotationState();
const status = ref('');
const first = computed(() => selected.value[0]);
const next = computed(() => {
  const wanted = first.value;
  const index = wanted
    ? pages.value.findIndex((page) => page.ref.objectNumber === wanted.page.objectNumber)
    : -1;
  return pages.value[index + 1];
});

async function copyToNextPage() {
  const source = first.value;
  const target = next.value;
  if (!source || !target) return;
  const copy = annotation.get(source.ref)!;
  const { annotation: made } = await annotation.create(target.ref, copy, undefined, {
    select: true, // so the next click copies the copy, a page further
  });
  stage.reveal(target.ref, { rect: made.rect });
  status.value = `Copied to page ${target.index + 1}`;
}
</script>

<template>
  <div class="toolbar">
    <button
      type="button"
      class="button"
      :disabled="selected.length !== 1 || !next"
      @click="copyToNextPage"
    >
      Copy to the next page
    </button>
    <span class="spacer" />
    <output class="readout">
      {{ status || (first ? 'Ready to copy' : 'Select an annotation to copy') }}
    </output>
  </div>
</template>
```

> **Try it:** copy the rectangle, then copy the copy: it moves a page further each time. Select the
> text box to copy it instead.

Stamps and file attachments also need their bytes. Download them first:

```ts
const drawing = await annotation.downloadResource(stamp.ref, 'appearance');
await annotation.create(otherPage.ref, stamp, { appearance: drawing });

const file = await annotation.downloadResource(attachment.ref, 'file');
await annotation.create(otherPage.ref, attachment, { file });
```

To copy on the **same page**, clear its unique name first: `{ ...copy, nm: null }`. A name is
unique per page, so a copy with a taken name is refused.

To copy many annotations at once, use [export and import](saving#export-and-import).

## Methods

What `useAnnotation()` gives you directly. Its parts have their own pages: `selection`, `draft` and
`text` are on [Selecting & editing](selecting#methods), `tools` and `stamps` on
[Tools & defaults](tools#methods), and `comments` on [Comments & replies](comments#methods).

| Method                                                         | What it does                                                                                                                                                                                                                                     |
| :------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `get(ref)`                                                     | One annotation as it shows now, pending changes included, or `null`                                                                                                                                                                              |
| `list({ pages?, subtype? })`                                   | Every annotation in drawing order, or the ones on some pages or of one kind. In a component, use `useAnnotationList(filter)`. [Read annotations](#read-annotations)                                                                              |
| `getAt(page, point)`                                           | The topmost annotation at a point on a page, or `null`. A selected annotation's handles count as the annotation.                                                                                                                                 |
| `isPending(ref)`                                               | Whether the engine is still saving a change to it. [Changes show at once](#changes-show-at-once)                                                                                                                                                 |
| `create(page, fields, resources?, { tool?, select? })`         | Creates an annotation from its kind and fields, and shows it at once. A stamp's drawing or an attachment's file goes in `resources`, as `{ appearance }` or `{ file }`. Resolves `{ annotation }`. [Create an annotation](#create-an-annotation) |
| `createFromSelection(tool, { clear? })`                        | Turns the selected text into a highlight, an underline, a strikeout or a squiggly line, one per page. Resolves `{ annotations }`. [From the selected text](#from-the-selected-text)                                                              |
| `update(ref, changes, resources?)`                             | Changes only the fields you pass, and `null` removes one. New `resources` replace a stamp's drawing or an attachment's file. Resolves `{ annotation }`. [Change an annotation](#change-an-annotation)                                            |
| `delete(ref)`                                                  | Deletes an annotation, with its popup and its replies. [Delete an annotation](#delete-an-annotation)                                                                                                                                             |
| `reorder(refs, position)`                                      | Moves annotations to a new place in their page's drawing order: next to another annotation, or to the back or front. [Change the drawing order](#change-the-drawing-order)                                                                       |
| `export({ refs?, pages? })`, `import(bundle, options?)`        | Takes annotations out as a bundle, with the bytes they carry, or puts a bundle in. `import` resolves `{ annotations, refMap, dropped }`. [Export and import](saving#export-and-import)                                                           |
| `downloadResource(ref, role)`                                  | Downloads a stamp's drawing (`'appearance'`) or an attachment's file (`'file'`), ready to pass back to `create()`. [Copy an annotation](#copy-an-annotation)                                                                                     |
| `links.get(ref)`, `links.set(ref, target)`, `links.clear(ref)` | Where an annotation links to, such as a page or a website: read it, set it, or remove the link                                                                                                                                                   |
| `refresh()`                                                    | Reads every annotation from the engine again. Changes arrive by themselves, so you rarely need it.                                                                                                                                               |
| `getSettings()`, `updateSettings(changes)`, `resetSettings()`  | Reads `snap`, `chrome` and `afterCreate`, changes them while the app runs, or goes back to what you registered. In a component, use `useAnnotationSettings()`. [Settings](../annotations#settings)                                               |
| `canRead()`, `canCreate()`, `canUpdate(ref)`, `canDelete(ref)` | Whether the user may see, create, change or delete annotations. The checks that take a `ref` answer for that annotation. [Permissions](#permissions)                                                                                             |

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

## Permissions

| Permission           | What it allows     | Check                       |
| :------------------- | :----------------- | :-------------------------- |
| `doc.annotate.read`  | Seeing annotations | `annotation.canRead()`      |
| `annotations:create` | Creating them      | `annotation.canCreate()`    |
| `annotations:update` | Changing them      | `annotation.canUpdate(ref)` |
| `annotations:delete` | Deleting them      | `annotation.canDelete(ref)` |

A permission can end in `:self`, the user's own annotations, or `:group`, their group's.
`annotations:update:self` lets someone change what they made and only look at the rest, so the checks
that take a `ref` answer per annotation. They also follow the annotation's own flags: for a `locked`
one, `canDelete(ref)` is false, so your UI offers no delete button, even though your own code may still
delete it (see [Locked annotations](#locked-annotations)). More on
[Permissions](../concepts/permissions).

## Good to know

### Locked annotations

The `locked` flag stops people from moving, resizing or deleting an annotation, and
`lockedContents` stops them from changing its text. These flags are for the people using your
app: your own code can still change a locked annotation, as the engine can. To stop the code
too, check the flags first.

### What a copy can't carry

A few annotations can't be made again from their read, and `create()` refuses them with
`invalid-input`:

- a link whose action the engine can't create, such as one that runs JavaScript;
- a popup whose annotation is gone;
- a value another app wrote that the engine can't write, such as a beveled border;
- a kind the engine doesn't know (`subtype: 'unsupported'`).

[Import](saving#export-and-import) leaves these out and tells you which ones, and why.

### Changes other people make

On a shared document, other people's changes arrive as events and show up in `list()` by
themselves. See [Events, saving & sharing](saving).
