---
title: "Comments & replies — Vue"
description: "Build a comments sidebar with threads, replies, review states and jumping to a comment."
framework: "Vue"
source: "https://www.embedpdf.com/docs/headless/vue/annotations/comments"
---

# Comments & replies

Any annotation can carry a comment, its `contents`, and people can reply to it. Together they
make a **thread**: the first comment and its replies, in order. `useCommentThreads()` gives you
every thread in the document, ready for a sidebar:

**`sidebar.vue`**

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

const threads = useCommentThreads();
</script>

<template>
  <article v-for="thread in threads" :key="annotationKey(thread.root.ref)">
    <p>{{ thread.root.author }}, page {{ thread.pageLabel }}: {{ thread.root.contents }}</p>
    <p v-for="reply in thread.replies" :key="annotationKey(reply.ref)">
      {{ reply.author }}: {{ reply.contents }}
    </p>
  </article>
</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 { SelectionLayer, selectionPlugin } from '@embedpdf/vue/selection';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/vue/annotation';
import { localEngine } from '@embedpdf/engine';
import Comments from './Comments.vue';

import '../comments.css';

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

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

<template>
  <Viewer
    :engine="engine"
    :plugins="plugins"
    :identity="{ userId: 'u_381', displayName: 'Dana Smith' }"
    :initial-documents="[{ source: proposal }]"
  >
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <div class="viewer">
        <Stage class="stage">
          <template #page>
            <RenderLayer />
            <SelectionLayer />
            <AnnotationLayer />
          </template>
        </Stage>
        <Comments />
      </div>
    </DocumentGate>
  </Viewer>
</template>
```

**`comments.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 threads: one card each */
.threads {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.thread {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px;
  background: var(--tint);
  border-radius: 10px;
}

.comment {
  margin: 0;
  font-size: 13px;
  line-height: 1.4;
  color: var(--text);
}

.comment strong {
  color: var(--ink);
}

.empty {
  margin: 0;
  font-size: 13px;
  color: var(--muted);
}

/* Where a thread is, its replies, and the reply box */
.thread-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

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

.show {
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  color: var(--accent);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

.reply {
  padding-left: 10px;
  border-left: 2px solid var(--line-strong);
}

.reply-form {
  margin: 0;
}

.field {
  box-sizing: border-box;
  width: 100%;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 6px 9px;
}

.field:focus-visible {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px rgb(125 182 255 / 0.35);
}

/* 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 280px;
    max-height: 420px;
  }
}

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

**`Comments.vue`**

```vue
<!-- Every thread in the document, in reading order. -->
<script setup lang="ts">
import { annotationKey, useAnnotationState, useCommentThreads } from '@embedpdf/vue/annotation';
import Thread from './Thread.vue';

const threads = useCommentThreads();
const loading = useAnnotationState((state) => state.status === 'loading');
</script>

<template>
  <div class="panel threads">
    <p v-if="loading" class="empty">Loading comments…</p>
    <p v-if="!loading && threads.length === 0" class="empty">No comments</p>
    <Thread v-for="thread in threads" :key="annotationKey(thread.root.ref)" :thread="thread" />
  </div>
</template>
```

**`Thread.vue`**

```vue
<!-- One thread: the first comment, its replies, a reply box, and a button that shows it. -->
<script setup lang="ts">
import { ref } from 'vue';
import { useStage } from '@embedpdf/vue/stage';
import { annotationKey, useAnnotation, useComments } from '@embedpdf/vue/annotation';
import type { CommentThreadView } from '@embedpdf/vue/annotation';

const { thread } = defineProps<{ thread: CommentThreadView }>();

const comments = useComments();
const annotation = useAnnotation();
const stage = useStage();
const text = ref('');

function show() {
  stage.reveal(thread.page, { rect: thread.root.rect });
  annotation.selection.set([thread.root.ref]); // and select it
}

function reply() {
  if (!text.value.trim()) return;
  void comments.reply(thread.root.ref, text.value.trim());
  text.value = '';
}
</script>

<template>
  <article class="thread">
    <header class="thread-head">
      <span class="where">Page {{ thread.pageLabel }}</span>
      <button type="button" class="show" @click="show">Show</button>
    </header>
    <p class="comment">
      <strong>{{ thread.root.author }}</strong> {{ thread.root.contents }}
    </p>
    <p v-for="each in thread.replies" :key="annotationKey(each.ref)" class="comment reply">
      <strong>{{ each.author }}</strong> {{ each.contents }}
    </p>
    <form v-if="comments.canReply(thread.root.ref)" class="reply-form" @submit.prevent="reply">
      <input v-model="text" class="field" aria-label="Reply" placeholder="Reply…" />
    </form>
  </article>
</template>
```

> **Try it:** you're Dana Smith, reviewing Ada's proposal. Reply to a thread, or press **Show** to
> go to its comment on the page.

Threads come in reading order: by page, then from the top of the page. The list updates by itself
when anyone adds, changes or deletes a comment, on this screen or, on a shared document, on
someone else's.

A thread has:

| Field       | What it is                                                                                        |
| :---------- | :------------------------------------------------------------------------------------------------ |
| `root`      | The annotation that starts the thread: a note, a highlight, a shape.                              |
| `replies`   | Every reply, oldest first. Replies to replies are included.                                       |
| `review`    | The review states people set, such as accepted or rejected (see [Review states](#review-states)). |
| `page`      | The page it's on.                                                                                 |
| `pageLabel` | The page's label to show, such as `'iv'` or `'7'`.                                                |

## Read a comment on the page

The annotation layer draws a note's icon and a highlight's color, not the comment: like the
sidebar, the window that shows it is yours. Build it from what you already have: the selected
annotation, its thread, and the annotation menu, which keeps your card next to 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 { SelectionLayer, selectionPlugin } from '@embedpdf/vue/selection';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/vue/annotation';
import { localEngine } from '@embedpdf/engine';
import NoteCard from './NoteCard.vue';

import '../note-card.css';

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

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

<template>
  <Viewer
    :engine="engine"
    :plugins="plugins"
    :identity="{ userId: 'u_381', displayName: 'Dana Smith' }"
    :initial-documents="[{ source: proposal }]"
  >
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <p class="hint">Click the note next to the title, or the highlighted sentence</p>
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <SelectionLayer />
          <AnnotationLayer />
        </template>
        <template #overlay><NoteCard /></template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`note-card.css`**

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

/* The card next to the clicked annotation: its comment, the replies, and a reply box */
.note-card {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 240px;
  padding: 12px 14px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: 0 12px 30px -12px rgb(7 32 76 / 0.35);
}

.comment {
  margin: 0;
  font-size: 13px;
  line-height: 1.45;
  color: var(--text);
}

.comment strong {
  display: block;
  color: var(--ink);
}

.reply {
  padding-left: 10px;
  border-left: 2px solid var(--line-strong);
}

.reply-form {
  margin: 0;
}

.field {
  box-sizing: border-box;
  width: 100%;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 6px 9px;
}

.field:focus-visible {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px rgb(125 182 255 / 0.35);
}

.hint {
  margin: 0 0 14px;
  font-size: 13px;
  color: var(--muted);
}

/* The pages */
.stage {
  height: 440px;
  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);
}
```

**`NoteCard.vue`**

```vue
<!--
  The clicked annotation's comment and its replies, in your own card next to it: a note, or any
  mark that carries a comment, such as a highlight. It shows while the annotation is selected.
-->
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import {
  AnnotationMenu,
  annotationKey,
  useAnnotation,
  useAnnotationState,
  useCommentThread,
  useComments,
} from '@embedpdf/vue/annotation';

const annotation = useAnnotation();
const comments = useComments();
const { selected } = useAnnotationState();
// No card without a comment to read: the template checks `thread?.root.contents`.
const thread = useCommentThread(() =>
  selected.value.length === 1 ? selected.value[0]!.ref : null,
);
const reply = ref('');

// Escape closes the card, as it ends the selection.
function close(event: KeyboardEvent) {
  if (event.key === 'Escape') annotation.selection.clear();
}

onMounted(() => window.addEventListener('keydown', close));
onUnmounted(() => window.removeEventListener('keydown', close));

function send() {
  if (!thread.value || !reply.value.trim()) return;
  void comments.reply(thread.value.root.ref, reply.value.trim());
  reply.value = '';
}
</script>

<template>
  <AnnotationMenu v-if="thread?.root.contents" placement="right">
    <article class="note-card">
      <p class="comment">
        <strong>{{ thread.root.author }}</strong> {{ thread.root.contents }}
      </p>
      <p v-for="each in thread.replies" :key="annotationKey(each.ref)" class="comment reply">
        <strong>{{ each.author }}</strong> {{ each.contents }}
      </p>
      <form v-if="comments.canReply(thread.root.ref)" class="reply-form" @submit.prevent="send">
        <input v-model="reply" class="field" aria-label="Reply" placeholder="Reply…" />
      </form>
    </article>
  </AnnotationMenu>
</template>
```

> **Try it:** click the note next to the title, or the highlighted sentence, then reply. Press
> **Escape** to close the card.

The card shows while the annotation is selected, so a click elsewhere closes it too. Any
annotation can carry a comment, so the same card opens for a note, a highlight or a shape.

## Reply, edit and delete

```ts
const { annotation: reply } = await comments.reply(thread.root.ref, 'Checked, looks right.');
await comments.setText(reply.ref, 'Checked against the invoice, looks right.');
await comments.delete(reply.ref); // one comment
await comments.deleteThread(thread.root.ref); // the whole thread
```

A reply always goes to the thread's first comment, whichever comment you pass. It's saved the way
Acrobat saves replies, so they show up as replies there too.

Deleting the first comment of a thread deletes the whole thread, as Acrobat does. `deleteThread()`
does the same, all or nothing, and tells you what happened: when the thread has a comment you may
not delete, such as someone else's reply, nothing is deleted and `failed` lists what stopped it.

## Review states

A review state is a verdict on a thread: accepted, rejected, cancelled or completed. Each person
sets their own, and a check mark is separate:

```ts
await comments.setStatus(thread.root.ref, 'accepted');
await comments.setMarked(thread.root.ref, true); // check it off

thread.review.lastChange?.state; // the latest verdict, from anyone
thread.review.byReviewer; // each person's latest verdict
thread.review.markedBy; // who has checked it off
```

States are saved as the PDF's own review states, so Acrobat shows them in its comment list.

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

import '../review.css';

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

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

<template>
  <Viewer
    :engine="engine"
    :plugins="plugins"
    :identity="{ userId: 'u_381', displayName: 'Dana Smith' }"
    :initial-documents="[{ source: proposal }]"
  >
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <div class="viewer">
        <Stage class="stage">
          <template #page>
            <RenderLayer />
            <SelectionLayer />
            <AnnotationLayer />
          </template>
        </Stage>
        <Reviews />
      </div>
    </DocumentGate>
  </Viewer>
</template>
```

**`review.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);
  --accepted: #1d7a4d;
  --accepted-tint: #d9f5e6;
  --rejected: #b42318;
  --rejected-tint: #fde2e1;
}

.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;
}

/* The threads: one card each */
.threads {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.thread {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px;
  background: var(--tint);
  border-radius: 10px;
}

.comment {
  margin: 0;
  font-size: 13px;
  line-height: 1.4;
  color: var(--text);
}

.comment strong {
  color: var(--ink);
}

.empty {
  margin: 0;
  font-size: 13px;
  color: var(--muted);
}

/* A thread's verdict and its buttons */
.verdict {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
}

.state {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  background: var(--surface);
  border-radius: 6px;
  padding: 2px 6px;
}

.state--accepted {
  color: var(--accepted);
  background: var(--accepted-tint);
}

.state--rejected {
  color: var(--rejected);
  background: var(--rejected-tint);
}

.mark {
  font-size: 12px;
  font-weight: 600;
  color: var(--accepted);
}

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.actions .button {
  padding: 5px 9px;
  font-size: 12px;
}

/* 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 280px;
    max-height: 420px;
  }
}

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

**`Review.vue`**

```vue
<!-- A thread's verdict, its check mark, and the buttons the user may use. -->
<script setup lang="ts">
import { computed } from 'vue';
import { useComments } from '@embedpdf/vue/annotation';
import type { CommentThreadView } from '@embedpdf/vue/annotation';

const { thread } = defineProps<{ thread: CommentThreadView }>();

const comments = useComments();
const root = computed(() => thread.root.ref); // the thread's first comment
const verdict = computed(() => thread.review.lastChange?.state ?? 'none');
const marked = computed(() => thread.review.markedBy.length > 0);
</script>

<template>
  <article class="thread">
    <p class="comment">
      <strong>{{ thread.root.author }}</strong> {{ thread.root.contents }}
    </p>
    <p class="verdict">
      <span :class="`state state--${verdict}`">{{ verdict }}</span>
      <span v-if="marked" class="mark">✓ checked off</span>
    </p>
    <div class="actions">
      <button
        type="button"
        class="button"
        :disabled="!comments.canSetStatus(root)"
        @click="comments.setStatus(root, 'accepted')"
      >
        Accept
      </button>
      <button
        type="button"
        class="button"
        :disabled="!comments.canSetStatus(root)"
        @click="comments.setStatus(root, 'rejected')"
      >
        Reject
      </button>
      <button
        type="button"
        class="button"
        :aria-pressed="marked"
        :disabled="!comments.canSetMarked(root)"
        @click="comments.setMarked(root, !marked)"
      >
        ✓
      </button>
      <button
        type="button"
        class="button"
        :disabled="!comments.canDeleteThread(root)"
        @click="comments.deleteThread(root)"
      >
        Delete
      </button>
    </div>
  </article>
</template>
```

**`Reviews.vue`**

```vue
<script setup lang="ts">
import { annotationKey, useCommentThreads } from '@embedpdf/vue/annotation';
import Review from './Review.vue';

const threads = useCommentThreads();
</script>

<template>
  <div class="panel threads">
    <p v-if="threads.length === 0" class="empty">No comments</p>
    <Review v-for="thread in threads" :key="annotationKey(thread.root.ref)" :thread="thread" />
  </div>
</template>
```

> **Try it:** accept or reject a comment, or check it off. Dana has already accepted Ada's note on
> page 2.

## Jump to a comment

The thread's `root` has a page and a `rect`, which is where the view should go:

**`jump.vue`**

```vue
<script setup lang="ts">
import { useAnnotation, type CommentThread } from '@embedpdf/vue/annotation';
import { useStage } from '@embedpdf/vue/stage';

const { thread } = defineProps<{ thread: CommentThread }>();

const stage = useStage();
const annotation = useAnnotation();

function show() {
  stage.reveal(thread.page, { rect: thread.root.rect });
  annotation.selection.set([thread.root.ref]); // and select it
}
</script>

<template>
  <button @click="show">Show</button>
</template>
```

## Methods

Everything `useComments()` gives you. It's the `comments` part of `useAnnotation()`.

| Method                                                                                                                 | What it does                                                                                                                                         |
| :--------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- |
| `listThreads()`                                                                                                        | Every thread, in reading order. In a component, use `useCommentThreads()`, which also gives each thread its `pageLabel`.                             |
| `getThread(ref)`                                                                                                       | The thread a comment is in, whether you pass its first comment, a reply or a review state, or `null`. In a component, use `useCommentThread(ref)`.   |
| `reply(ref, text)`                                                                                                     | Replies to the thread's first comment, whichever comment you pass. Resolves `{ annotation }`. [Reply, edit and delete](#reply-edit-and-delete)       |
| `setText(ref, text)`                                                                                                   | Changes a comment's text                                                                                                                             |
| `delete(ref)`                                                                                                          | Deletes a comment with its replies. The first comment of a thread deletes the whole thread.                                                          |
| `deleteThread(ref)`                                                                                                    | Deletes a whole thread, all or nothing. Resolves `{ deleted, failed }`: when a comment in it may not be deleted, nothing goes and `failed` lists it. |
| `setStatus(ref, state)`                                                                                                | Sets the user's review state on a thread, such as `'accepted'` or `'rejected'`. [Review states](#review-states)                                      |
| `setMarked(ref, marked)`                                                                                               | Checks a thread off for the user, or clears the check mark                                                                                           |
| `canReply(ref)`, `canSetText(ref)`, `canDelete(ref)`, `canDeleteThread(ref)`, `canSetStatus(ref)`, `canSetMarked(ref)` | Whether the user may do that to this comment. [Permissions](#permissions)                                                                            |

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

## Events

**`events.vue`**

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

useAnnotationEvent(
  (annotation) => annotation.comments.onThreadChanged,
  ({ rootRef, change }) => change === 'reply' && notifyThreadAuthor(rootRef),
);
</script>

<template>
  <aside><!-- … --></aside>
</template>
```

| Event                      | Fires when                           | Carries             |
| :------------------------- | :----------------------------------- | :------------------ |
| `comments.onThreadChanged` | A thread changed through this viewer | `rootRef`, `change` |

`change` is `'reply'`, `'text'`, `'status'`, `'marked'` or `'deleted'`. For every change, including
other people's, use the annotation events; see [Events, saving & sharing](saving#events).

## Permissions

Every comment verb has a check that answers for one comment, so you show only the buttons that work:

**`permissions.vue`**

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

const { thread } = defineProps<{ thread: CommentThread }>();
const comments = useComments();
</script>

<template>
  <button :disabled="!comments.canReply(thread.root.ref)">Reply</button>
  <button :disabled="!comments.canDeleteThread(thread.root.ref)">Delete thread</button>
</template>
```

| Permission           | What it allows                                           | Check                                                     |
| :------------------- | :------------------------------------------------------- | :-------------------------------------------------------- |
| `annotations:create` | Replying, setting a review state, checking a comment off | `canReply(ref)`, `canSetStatus(ref)`, `canSetMarked(ref)` |
| `annotations:update` | Changing a comment's text                                | `canSetText(ref)`                                         |
| `annotations:delete` | Deleting a comment, or every comment in a thread         | `canDelete(ref)`, `canDeleteThread(ref)`                  |

A review state and a check mark are small replies of their own, the way Acrobat stores them, so they
need the right to create, not to change. The checks also follow the comment's own flags:
`lockedContents` stops text edits, and `locked` stops deleting.

## Good to know

### Comments without a note

Any annotation can start a thread. A highlight with a comment is a thread whose `root` is the
highlight. A plain note (the `note` tool) is a thread with an icon on the page.

### Loading

The `status` from `useAnnotationState()` is `'loading'` until every annotation is in. Show a
loading state in your sidebar until then, so an empty list doesn't look like "no comments".

### Reviewing together

On the local engine, a review stays with the file: the next person sees the comments once they have
the downloaded PDF.

> **Turn comments into a live review**
>
> With CloudPDF, everyone with the document open sees new comments, replies and review states as they
> happen, and the server checks who may reply, change or delete what. Your sidebar stays the same:
> `useCommentThreads()` updates by itself.
>
> [Review with CloudPDF](https://www.cloudpdf.com/docs/headless/vue/annotations/comments)
