---
title: "Selecting & editing — Vue"
description: "How people select and change annotations, and how to add a menu, a style panel and your own handles."
framework: "Vue"
source: "https://www.embedpdf.com/docs/headless/vue/annotations/selecting"
---

# Selecting & editing

With the `pointer` tool active, people click an annotation to select it. A box with handles appears
around it: drag the annotation to move it, a handle to resize it, and the round handle above it to
turn it. Your code reads and changes the same selection, and you build the menu and the style
panel around it.

## How people select

- **Click** an annotation to select it. **Shift-click** adds it to the selection, or takes it out.
- **Drag on empty space** to draw a box: everything it touches is selected. It has to touch what an annotation paints: its lines, or the inside of a filled shape. A box drawn inside an empty rectangle doesn't select the rectangle.
- **Click empty space** to clear the selection.
- **Double-click** a text box to type in it.

While moving, the selection snaps to the edges and centers of other annotations and of the page,
and a turn snaps to 0°, 90°, 180° and 270°. Hold Shift to move or turn freely. See
[Snapping](#snapping) to change it.

## Select with code

```ts
annotation.selection.set([ref]); // select these
annotation.selection.add([otherRef]); // add to the selection
annotation.selection.selectAll(page.ref); // everything on a page (or leave out the page)
annotation.selection.selectInRect(page.ref, { x: 50, y: 50, width: 200, height: 100 });
annotation.selection.clear();
```

To show the selection in a component, use `useAnnotationState()`. Your component
updates when the selection changes:

**`selected.vue`**

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

const { selected } = useAnnotationState(); // the selected annotations
</script>

<template>
  <p>{{ selected.length }} selected</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 AddShapes from './AddShapes.vue';
import SelectionToolbar from './SelectionToolbar.vue';

import '../select-code.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>
      <AddShapes />
      <SelectionToolbar />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <AnnotationLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`select-code.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);
}
```

**`AddShapes.vue`**

```vue
<script setup lang="ts">
import { computed, watch } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import { useAnnotation, useAnnotationState } from '@embedpdf/vue/annotation';

// On load: two shapes at the top of the cover, two at the bottom, and all four selected.
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;
    void Promise.all([
      annotation.create(page, {
        subtype: 'square',
        box: { x: 60, y: 40, width: 120, height: 50 },
        color: '#e5484d',
        strokeWidth: 3,
      }),
      annotation.create(page, {
        subtype: 'circle',
        box: { x: 490, y: 200, width: 80, height: 80 },
        color: '#1e90ff',
        strokeWidth: 3,
      }),
      annotation.create(page, {
        subtype: 'square',
        box: { x: 96, y: 506, width: 178, height: 54 },
        color: '#30a46c',
        strokeWidth: 3,
      }),
      annotation.create(page, {
        subtype: 'text',
        rect: { x: 480, y: 640, width: 20, height: 20 },
        contents: 'A note at the bottom',
        color: '#facc15',
      }),
    ]).then(() => annotation.selection.selectAll(page));
  },
  { immediate: true },
);
</script>

<template></template>
```

**`SelectionToolbar.vue`**

```vue
<script setup lang="ts">
import { computed } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import { useAnnotation, useAnnotationState } from '@embedpdf/vue/annotation';

// The top half of the cover, where the box selects.
const TOP_HALF = { x: 0, y: 0, width: 612, height: 396 };

const annotation = useAnnotation();
const { selected } = useAnnotationState(); // the selected annotations
const pages = usePageList();
const cover = computed(() => pages.value[0]?.ref);

function selectAll() {
  if (cover.value) annotation.selection.selectAll(cover.value);
}

function selectTopHalf() {
  if (cover.value) annotation.selection.selectInRect(cover.value, TOP_HALF);
}
</script>

<template>
  <div class="toolbar">
    <button type="button" class="button" :disabled="!cover" @click="selectAll">
      Everything on the cover
    </button>
    <button type="button" class="button" :disabled="!cover" @click="selectTopHalf">
      The top half
    </button>
    <button
      type="button"
      class="button"
      :disabled="selected.length === 0"
      @click="annotation.selection.clear()"
    >
      Clear
    </button>
    <span class="spacer" />
    <output class="readout">{{ selected.length }} selected</output>
  </div>
</template>
```

> **Try it:** **The top half** selects the two shapes the box touches. Shift-click a shape to add
> it, or take it out.

Every field is listed under [State](#state).

## Add a menu

`<AnnotationMenu>` places your own UI next to the selection. Mount it in the Stage's `#overlay` slot,
and it stays attached while the user scrolls and zooms:

**`menu.vue`**

```vue
<script setup lang="ts">
import { computed } from 'vue';
import {
  AnnotationLayer,
  AnnotationMenu,
  useAnnotation,
  useAnnotationState,
} from '@embedpdf/vue/annotation';
import { RenderLayer } from '@embedpdf/vue/render';
import { Stage } from '@embedpdf/vue/stage';

const annotation = useAnnotation();
const { selected } = useAnnotationState();
const allText = computed(() => selected.value.every((a) => a.subtype === 'free-text'));
</script>

<template>
  <Stage>
    <template #page>
      <RenderLayer />
      <AnnotationLayer />
    </template>

    <template #overlay>
      <AnnotationMenu placement="bottom">
        <div class="menu">
          <button v-if="allText" @click="annotation.text.toggleFormat('bold')">Bold</button>
          <button @click="annotation.selection.update({ color: '#dc143c' })">Red</button>
          <button @click="annotation.selection.delete()">Delete</button>
        </div>
      </AnnotationMenu>
    </template>
  </Stage>
</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, AnnotationMenu, annotationPlugin } from '@embedpdf/vue/annotation';
import { localEngine } from '@embedpdf/engine';
import AddAnnotations from './AddAnnotations.vue';
import SelectionActions from './SelectionActions.vue';

import '../menu.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 />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <AnnotationLayer />
        </template>
        <template #overlay>
          <AnnotationMenu placement="bottom">
            <SelectionActions />
          </AnnotationMenu>
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`menu.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 menu next to the selection */
.menu {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  padding: 4px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: 0 10px 28px -10px rgb(7 32 76 / 0.3);
}

.menu button {
  font: inherit;
  font-size: 12.5px;
  font-weight: 550;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-radius: 7px;
  padding: 6px 10px;
  cursor: pointer;
  white-space: nowrap;
}

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

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

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

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

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

**`AddAnnotations.vue`**

```vue
<script setup lang="ts">
import { computed, watch } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import { useAnnotation, useAnnotationState } from '@embedpdf/vue/annotation';

// On load: a rectangle and a text box on the cover, the text box selected so the menu shows.
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;
    void annotation.create(page, {
      subtype: 'square',
      box: { x: 96, y: 506, width: 178, height: 54 },
      color: '#1e90ff',
      strokeWidth: 3,
    });
    void annotation.create(
      page,
      {
        subtype: 'free-text',
        box: { x: 300, y: 512, width: 230, height: 40 },
        contents: 'Ready for review',
        fontSize: 16,
        fontColor: '#1a2748',
        interiorColor: '#fffbe6',
      },
      undefined,
      { select: true },
    );
  },
  { immediate: true },
);
</script>

<template></template>
```

**`SelectionActions.vue`**

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

// What's in the menu depends on what's selected: text boxes get Bold and Edit.
const annotation = useAnnotation();
const { selected } = useAnnotationState();
const allText = computed(() => selected.value.every((a) => a.subtype === 'free-text'));
const first = computed(() => selected.value[0]);
</script>

<template>
  <div class="menu" role="toolbar" aria-label="Selection">
    <button v-if="allText" type="button" @click="annotation.text.toggleFormat('bold')">Bold</button>
    <button
      v-if="allText && selected.length === 1 && first"
      type="button"
      @click="annotation.text.begin(first.ref)"
    >
      Edit text
    </button>
    <button type="button" @click="annotation.selection.update({ color: '#dc143c' })">Red</button>
    <button type="button" @click="annotation.selection.delete()">Delete</button>
  </div>
</template>
```

> **Try it:** select the rectangle: the menu loses **Bold** and **Edit text**. Drag it, and the
> menu hides until you let go.

- **The menu only places things.** What's in it is yours: build it from the selection, as above,
  to show buttons that fit what's selected.
- **It moves out of the way.** It hides while the user drags, and it never covers the rotation
  handle.
- **`placement`** is `'top'` (the default), `'bottom'`, `'left'` or `'right'`, and `gap` is the
  space between the selection and the menu, in pixels.

## A style panel

A style panel shows the properties of what's selected, such as color, width, opacity and font, with
their current values. `useAnnotationProperties()` gives you everything to draw it:

- **`properties`** are what the selection can change, in display order. Each one says which control
  fits: a color, a number with its range, a choice from a list, or an on/off flag.
- **`values`** are their current values.
- **`mixed`** lists the properties where the selected annotations differ. Show those as "mixed".

When different kinds are selected, you get only the properties they all have.

Each property's `key` is the annotation's own field name, so most read and write with no mapping.
Two group several fields: `borderStyle` is the style, the dashes and the cloudy edge together, and
`lineEndings` reads as a `{ start, end }` pair while its options are single endings. The panel below
shows both:

**`style-panel.vue`**

```vue
<script setup lang="ts">
import { useAnnotation, useAnnotationProperties } from '@embedpdf/vue/annotation';
import Control from './Control.vue'; // your own control

const annotation = useAnnotation();
const panel = useAnnotationProperties(); // a ref: { properties, values, mixed }
</script>

<template>
  <Control
    v-for="property in panel.properties"
    :key="property.key"
    :property="property"
    :value="panel.values[property.key]"
    :mixed="panel.mixed.includes(property.key)"
    @change="(value: unknown) => annotation.selection.update({ [property.key]: value })"
  />
</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 StylePanel from './StylePanel.vue';

import '../style-panel.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>
        <StylePanel />
      </div>
    </DocumentGate>
  </Viewer>
</template>
```

**`style-panel.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);
}

/* Any color */
.color {
  width: 34px;
  height: 30px;
  padding: 2px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  cursor: pointer;
}

/* The style panel: a row per property */
.properties {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.property {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.property dt {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text);
}

.property dd {
  display: flex;
  align-items: center;
  margin: 0;
}

.property input[type='checkbox'] {
  width: 16px;
  height: 16px;
  margin: 0;
  accent-color: var(--accent);
}

.pair {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.pair input[type='range'] {
  width: 90px;
  accent-color: var(--accent);
}

.value {
  min-width: 30px;
  font-size: 12px;
  text-align: right;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.select,
.text {
  max-width: 130px;
  font: inherit;
  font-size: 12.5px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 7px;
  padding: 4px 6px;
}

.toggle {
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--muted);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 7px;
  padding: 4px 10px;
  cursor: pointer;
}

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

.link {
  font: inherit;
  font-size: 12px;
  color: var(--accent);
  background: none;
  border: 0;
  padding: 0;
  text-decoration: underline;
  cursor: pointer;
}

.empty {
  font-size: 13px;
  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
<script setup lang="ts">
import { computed, watch } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import { useAnnotation, useAnnotationState } from '@embedpdf/vue/annotation';

// On load: a rectangle (selected), an arrow and a text box on the cover.
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;
    void annotation.create(page, {
      subtype: 'line',
      linePoints: { start: { x: 520, y: 120 }, end: { x: 470, y: 230 } },
      lineEndings: { start: 'none', end: 'closed-arrow' },
      color: '#1a2748',
      strokeWidth: 3,
    });
    void annotation.create(page, {
      subtype: 'free-text',
      box: { x: 300, y: 512, width: 230, height: 40 },
      contents: 'Ready for review',
      fontSize: 16,
      fontColor: '#1a2748',
    });
    void annotation.create(
      page,
      {
        subtype: 'square',
        box: { x: 96, y: 506, width: 178, height: 54 },
        color: '#e5484d',
        interiorColor: '#ffe4e1',
        strokeWidth: 3,
      },
      undefined,
      { select: true },
    );
  },
  { immediate: true },
);
</script>

<template></template>
```

**`Control.vue`**

```vue
<script setup lang="ts">
import { computed } from 'vue';
import { useAnnotation, useAnnotationProperties } from '@embedpdf/vue/annotation';
import type { AnnotationProperty, LineEnding } from '@embedpdf/vue/annotation';

// One control per property, by the control it asks for.
const props = defineProps<{ property: AnnotationProperty }>();

const annotation = useAnnotation();
const panel = useAnnotationProperties();
const value = computed(() => panel.value.values[props.property.key]);
const isMixed = computed(() => panel.value.mixed.includes(props.property.key));

const update = (next: unknown) => annotation.selection.update({ [props.property.key]: next });
/** What the control holds now, from its `input` or `change` event. */
const valueOf = (event: Event) => (event.target as HTMLInputElement | HTMLSelectElement).value;

// Line endings are a pair: this sets the end, and each line keeps its own start.
const lineEnd = computed(() => (value.value as { end?: string } | undefined)?.end ?? 'none');
function setLineEnd(event: Event) {
  const next = valueOf(event) as LineEnding;
  void annotation.selection.update((member) =>
    member.subtype === 'line' ? { lineEndings: { ...member.lineEndings, end: next } } : {},
  );
}

// The border is three fields: its style, its dashes, and how cloudy it is.
const border = computed(() =>
  panel.value.values.cloudyIntensity ? 'cloudy' : String(value.value),
);
function setBorder(event: Event) {
  const next = valueOf(event);
  void annotation.selection.update({
    borderStyle: next === 'dashed' ? 'dashed' : 'solid',
    dashArray: next === 'dashed' ? [4, 3] : null,
    cloudyIntensity: next === 'cloudy' ? 1 : null,
  });
}

function setLink(event: Event) {
  void annotation.selection.updateLink(
    valueOf(event) === 'website' ? { kind: 'uri', uri: 'https://www.embedpdf.com' } : null,
  );
}
</script>

<template>
  <span v-if="property.control === 'color'" class="pair">
    <input
      type="color"
      class="color"
      :aria-label="property.label"
      :value="typeof value === 'string' ? value : '#ffffff'"
      @input="update(valueOf($event))"
    />
    <button
      v-if="property.key === 'interiorColor'"
      type="button"
      class="link"
      @click="update(null)"
    >
      {{ value === null ? 'none' : 'remove' }}
    </button>
  </span>
  <span v-else-if="property.control === 'number'" class="pair">
    <input
      type="range"
      :aria-label="property.label"
      :min="property.min"
      :max="property.max"
      :step="property.step"
      :value="typeof value === 'number' ? value : property.min"
      @input="update(Number(valueOf($event)))"
    />
    <output class="value">{{ isMixed ? 'mixed' : String(value) }}</output>
  </span>
  <select
    v-else-if="property.control === 'choice' && property.key === 'lineEndings'"
    class="select"
    aria-label="Line end"
    :value="lineEnd"
    @change="setLineEnd"
  >
    <option v-for="option in property.options" :key="option.value" :value="option.value">
      {{ option.label }}
    </option>
  </select>
  <select
    v-else-if="property.control === 'choice' && property.key === 'borderStyle'"
    class="select"
    :aria-label="property.label"
    :value="isMixed ? '' : border"
    @change="setBorder"
  >
    <option v-if="isMixed" value="">mixed</option>
    <option v-for="option in property.options" :key="option.value" :value="option.value">
      {{ option.label }}
    </option>
  </select>
  <select
    v-else-if="property.control === 'choice'"
    class="select"
    :aria-label="property.label"
    :value="isMixed ? '' : String(value ?? '')"
    @change="update(valueOf($event))"
  >
    <option v-if="isMixed" value="">mixed</option>
    <option v-for="option in property.options" :key="option.value" :value="option.value">
      {{ option.label }}
    </option>
  </select>
  <input
    v-else-if="property.control === 'flag'"
    type="checkbox"
    :aria-label="property.label"
    :checked="value === true"
    @change="update(($event.target as HTMLInputElement).checked)"
  />
  <button
    v-else-if="property.control === 'textFormat'"
    type="button"
    class="toggle"
    :aria-pressed="value === true"
    @click="annotation.text.toggleFormat(property.format)"
  >
    {{ property.label }}
  </button>
  <input
    v-else-if="property.control === 'text'"
    class="text"
    :aria-label="property.label"
    :value="typeof value === 'string' ? value : ''"
    @input="update(valueOf($event))"
  />
  <select
    v-else-if="property.control === 'link'"
    class="select"
    :aria-label="property.label"
    :value="value ? 'website' : 'none'"
    @change="setLink"
  >
    <option value="none">No link</option>
    <option value="website">embedpdf.com</option>
  </select>
</template>
```

**`StylePanel.vue`**

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

// What the selection can change, with its current values.
const panel = useAnnotationProperties();
</script>

<template>
  <p v-if="panel.properties.length === 0" class="panel empty">Select an annotation</p>
  <dl v-else class="panel properties">
    <div v-for="property in panel.properties" :key="property.key" class="property">
      <dt>{{ property.label }}</dt>
      <dd><Control :property="property" /></dd>
    </div>
  </dl>
</template>
```

> **Try it:** change the rectangle's colors and width, then select the arrow or the text box: the
> panel shows what each one has. Shift-click two to see what they share.

| `property.control` | What to show                                                                                                      | Also on the property |
| :----------------- | :---------------------------------------------------------------------------------------------------------------- | :------------------- |
| `'color'`          | A color picker. `null` means no color.                                                                            |                      |
| `'number'`         | A slider or a number input.                                                                                       | `min`, `max`, `step` |
| `'choice'`         | A list, such as fonts or line endings. Each option is `{ value, label, labelKey }`: write `value`, show `label`.  | `options`            |
| `'flag'`           | An on/off switch, such as `locked` or `print`.                                                                    |                      |
| `'text'`           | A text input, such as a redaction's label.                                                                        |                      |
| `'textFormat'`     | A bold, italic or underline toggle for text boxes. Write it with `annotation.text.toggleFormat(property.format)`. | `format`             |
| `'link'`           | Where the annotation links to. Write it with `annotation.selection.updateLink(target)`, or `null` to remove it.   |                      |

`selection.update()` gives each selected annotation the properties its kind has, and skips the rest.
To change each annotation relative to itself, pass a function. For example, to set only the end
arrow of lines that keep their own start:

```ts
await annotation.selection.update((member) =>
  member.subtype === 'line' ? { lineEndings: { ...member.lineEndings, end: 'open-arrow' } } : {},
);
```

The same properties drive a panel for a tool's defaults; see
[Let people change the defaults](tools#let-people-change-the-defaults).

### In the user's language

Each property has a `label` in English and a `labelKey` to translate it, and so does each option:
`annotation.style.stroke`, `annotation.flags.noView`, `annotation.lineEndings.openArrow`. With the
[i18n plugin](../ui/translations), show `t(property.labelKey, { fallback: property.label })`, and
give your languages strings for the keys; without a string for a key, the English shows. A key names
the wording, not the field: `color` is `annotation.style.stroke` on a rectangle and
`annotation.style.outline` on a redaction mark, so each kind keeps its own word.

## Change the handles

By default the selection has an outline in the accent color, square handles on its corners and sides
(or on each point of a line or a polygon), and a round rotation handle above it, on a short line
called its stalk. Everything about them can change.

**Set them when you register the plugin.** One color changes every part; the rest is optional:

```ts
annotationPlugin({
  chrome: {
    accent: '#e91e63',
    outline: { style: 'dashed', width: 1 },
    handles: { shape: 'circle', size: 10 },
    rotationHandle: { offset: 30, stalk: false },
  },
});
```

| Setting          | What it changes                                                                                                                            | From CSS                                                                                                                       |
| :--------------- | :----------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------- |
| `accent`         | The color of every part below, unless you set one of its own. Defaults to the viewer's accent.                                             | `--epdf-annotation-accent`                                                                                                     |
| `outline`        | The box around the selection: `style` (`'solid'`, `'dashed'`), `width`, `color`.                                                           | `--epdf-annotation-outline`, `--epdf-annotation-outline-width`, `--epdf-annotation-outline-dash`                               |
| `handles`        | The resize and point handles: `shape` (`'square'`, `'circle'`), `size`, `hitSize`, `fill`, `stroke`.                                       | `--epdf-annotation-handle-fill`, `--epdf-annotation-handle-stroke`                                                             |
| `rotationHandle` | The turn handle: `enabled`, `size`, `hitSize`, `offset` (distance from the box), `stalk` (`false` hides the line to it), `fill`, `stroke`. | `--epdf-annotation-rotation-handle-fill`, `--epdf-annotation-rotation-handle-stroke`                                           |
| `guides`         | The lines shown while snapping and turning: `enabled`, `color`, `rotationColor`, `style`, `width`.                                         | `--epdf-annotation-guide`, `--epdf-annotation-rotation-guide`, `--epdf-annotation-guide-width`, `--epdf-annotation-guide-dash` |
| `marquee`        | The box you drag to select several annotations: `fill`, `stroke`.                                                                          | `--epdf-annotation-marquee-fill`, `--epdf-annotation-marquee-stroke`                                                           |
| `textOutline`    | The outline of a text box while you type in it.                                                                                            | `--epdf-annotation-text-outline`                                                                                               |
| `readout`        | The angle while you turn: `enabled`, `background`, `color`.                                                                                | `--epdf-readout-background`, `--epdf-readout-color`                                                                            |

Every size is in screen pixels, so handles stay the same size at every zoom. `hitSize` is the area
you can grab; keep it at 24 or more for touch.

**Override the colors from CSS**, for example for dark mode. A CSS variable wins over the setting:

```css
@media (prefers-color-scheme: dark) {
  .pdf-viewer {
    --epdf-annotation-accent: #ff80ab;
  }
}
```

Colors, widths and dashes can come from CSS. Sizes and the offset can't, because they decide where
you can grab. Every variable is on [Colors & theming](../ui/theming#every-color).

**Change them while the app runs**, for example when the user picks another accent:

```ts
annotation.updateSettings({ chrome: { accent: '#0f6e56' } });
annotation.resetSettings(); // back to what the plugin was registered with
```

**Draw them yourself.** Fill the layer's `#handle` and `#rotation-handle` slots, and it draws
them in place of its handles:

**`handles.vue`**

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

<template>
  <AnnotationLayer>
    <template #handle="{ at, size, rotation }">
      <div
        class="my-handle"
        :style="{
          left: `${at.x - size / 2}px`,
          top: `${at.y - size / 2}px`,
          width: `${size}px`,
          height: `${size}px`,
          rotate: `${rotation}deg`,
        }"
      />
    </template>
    <!-- #rotation-handle works the same way -->
  </AnnotationLayer>
</template>
```

| Prop       | What it is                                                          |
| :--------- | :------------------------------------------------------------------ |
| `at`       | The handle's center, in pixels on the page.                         |
| `size`     | The `size` from the settings.                                       |
| `rotation` | The selection's rotation, so a square handle can turn with it.      |
| `kind`     | `'corner'`, `'side'` or `'point'` (a point of a line or a polygon). |
| `active`   | True while it's being dragged.                                      |

The rotation handle gets the same props except `kind`, plus `from`, where its stalk starts on the box. You only draw:
the viewer still decides where a handle can be grabbed, so what you draw can't break dragging.

**`App.vue`**

```vue
<script setup lang="ts">
import { ref } from 'vue';
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/vue/annotation';
import { localEngine } from '@embedpdf/engine';
import AddRectangle from './AddRectangle.vue';
import ChromeControls from './ChromeControls.vue';
import Handle from './Handle.vue';
import RotationHandle from './RotationHandle.vue';

import '../handles.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()) };
};

const own = ref(false);
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <AddRectangle />
      <ChromeControls v-model:own="own" />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <!-- Your own handles: the layer places them, and still decides where they can be grabbed. -->
          <AnnotationLayer>
            <template v-if="own" #handle="handle"><Handle v-bind="handle" /></template>
            <template v-if="own" #rotation-handle="handle">
              <RotationHandle v-bind="handle" />
            </template>
          </AnnotationLayer>
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`handles.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);
  --handle: #e91e63;
  --handle-active: #ad1457;
}

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

/* Colors to pick from: the pressed one is the current one */
.swatches {
  display: inline-flex;
  gap: 6px;
}

.swatch {
  width: 26px;
  height: 26px;
  padding: 0;
  border: 2px solid var(--surface);
  border-radius: 50%;
  box-shadow: 0 0 0 1px var(--line);
  cursor: pointer;
}

.swatch[aria-pressed='true'] {
  box-shadow: 0 0 0 2px var(--ink);
}

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

/* A switch: a checkbox with its label */
.check {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  font-weight: 550;
  color: var(--ink);
  cursor: pointer;
}

.check input {
  width: 16px;
  height: 16px;
  margin: 0;
  accent-color: var(--accent);
}

/* Your own handles: the layer places them, in pixels on the page */
.handle {
  position: absolute;
  box-sizing: border-box;
  background: var(--surface);
  border: 2px solid var(--handle);
  border-radius: 3px;
}

.handle--side {
  border-radius: 50%;
}

.handle--active {
  background: var(--handle);
}

.turn {
  position: absolute;
  box-sizing: border-box;
  display: grid;
  place-items: center;
  font-size: 13px;
  line-height: 1;
  color: var(--surface);
  background: var(--handle);
  border-radius: 50%;
}

.turn--active {
  background: var(--handle-active);
}

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

**`AddRectangle.vue`**

```vue
<script setup lang="ts">
import { computed, watch } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import { useAnnotation, useAnnotationState } from '@embedpdf/vue/annotation';

// On load: a rectangle on the cover, selected so its handles show.
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;
    void annotation.create(
      page,
      {
        subtype: 'square',
        box: { x: 96, y: 506, width: 178, height: 54 },
        color: '#1a2748',
        strokeWidth: 2,
      },
      undefined,
      { select: true },
    );
  },
  { immediate: true },
);
</script>

<template></template>
```

**`ChromeControls.vue`**

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

const ACCENTS = ['#054fb3', '#e91e63', '#0f6e56'];

/** Whether the layer draws your handles. */
const own = defineModel<boolean>('own', { required: true });

const annotation = useAnnotation();
const chrome = useAnnotationSettings((settings) => settings.chrome);

const checked = (event: Event) => (event.target as HTMLInputElement).checked;
</script>

<template>
  <div class="toolbar">
    <div class="swatches" role="group" aria-label="Accent">
      <button
        v-for="accent in ACCENTS"
        :key="accent"
        type="button"
        class="swatch"
        :aria-label="accent"
        :aria-pressed="chrome.accent === accent"
        :style="{ background: accent }"
        @click="annotation.updateSettings({ chrome: { accent } })"
      />
    </div>
    <label class="check">
      <input
        type="checkbox"
        :checked="chrome.outline.style === 'dashed'"
        @change="
          annotation.updateSettings({
            chrome: { outline: { style: checked($event) ? 'dashed' : 'solid' } },
          })
        "
      />
      Dashed outline
    </label>
    <label class="check">
      <input
        type="checkbox"
        :checked="chrome.handles.shape === 'circle'"
        @change="
          annotation.updateSettings({
            chrome: { handles: { shape: checked($event) ? 'circle' : 'square' } },
          })
        "
      />
      Round handles
    </label>
    <label class="check">
      <input v-model="own" type="checkbox" />
      Draw them myself
    </label>
    <button type="button" class="button" @click="annotation.resetSettings()">Reset</button>
  </div>
</template>
```

**`Handle.vue`**

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

const { at, size, rotation, kind, active } = defineProps<HandleProps>();
</script>

<template>
  <div
    :class="['handle', `handle--${kind}`, { 'handle--active': active }]"
    :style="{
      left: `${at.x - size / 2}px`,
      top: `${at.y - size / 2}px`,
      width: `${size}px`,
      height: `${size}px`,
      rotate: `${rotation}deg`,
    }"
  />
</template>
```

**`RotationHandle.vue`**

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

const { at, size, active } = defineProps<RotationHandleProps>();
</script>

<template>
  <div
    :class="['turn', { 'turn--active': active }]"
    :style="{
      left: `${at.x - size}px`,
      top: `${at.y - size}px`,
      width: `${size * 2}px`,
      height: `${size * 2}px`,
    }"
  >
    ↻
  </div>
</template>
```

> **Try it:** pick an accent and round handles, then **Draw them myself**. **Reset** goes back to
> what the plugin was registered with.

## Snapping

```ts
annotationPlugin({
  snap: {
    alignment: true, // snap to other annotations and the page while moving
    alignmentThreshold: 6, // how close, in screen pixels, before it snaps
    rotation: true,
    rotationAngles: [0, 45, 90, 135, 180, 225, 270, 315],
    rotationThreshold: 4, // degrees
  },
});

annotation.updateSettings({ snap: { alignment: false } }); // while the app runs
```

The threshold is in screen pixels, so snapping feels the same at every zoom. The lines that show
what an annotation snaps to are `--epdf-annotation-guide`.

**`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 SnapControls from './SnapControls.vue';

import '../snapping.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 />
      <SnapControls />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <AnnotationLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`snapping.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;
}

/* A switch: a checkbox with its label */
.check {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  font-weight: 550;
  color: var(--ink);
  cursor: pointer;
}

.check input {
  width: 16px;
  height: 16px;
  margin: 0;
  accent-color: var(--accent);
}

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

/* 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
<script setup lang="ts">
import { computed, watch } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import { useAnnotation, useAnnotationState } from '@embedpdf/vue/annotation';

// On load: two rectangles on the cover, the right one selected. Drag it next to the other.
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;
    void annotation.create(page, {
      subtype: 'square',
      box: { x: 96, y: 506, width: 178, height: 54 },
      color: '#1e90ff',
      strokeWidth: 3,
    });
    void annotation.create(
      page,
      {
        subtype: 'square',
        box: { x: 340, y: 560, width: 120, height: 80 },
        color: '#e5484d',
        strokeWidth: 3,
      },
      undefined,
      { select: true },
    );
  },
  { immediate: true },
);
</script>

<template></template>
```

**`SnapControls.vue`**

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

const QUARTERS = [0, 90, 180, 270];
const EIGHTHS = [0, 45, 90, 135, 180, 225, 270, 315];

const annotation = useAnnotation();
const snap = useAnnotationSettings((settings) => settings.snap);

const checked = (event: Event) => (event.target as HTMLInputElement).checked;
</script>

<template>
  <div class="toolbar">
    <label class="check">
      <input
        type="checkbox"
        :checked="snap.alignment"
        @change="annotation.updateSettings({ snap: { alignment: checked($event) } })"
      />
      Snap to other annotations
    </label>
    <label class="check">
      <input
        type="checkbox"
        :checked="snap.rotationAngles.length === EIGHTHS.length"
        @change="
          annotation.updateSettings({
            snap: { rotationAngles: checked($event) ? EIGHTHS : QUARTERS },
          })
        "
      />
      Turns snap every 45°
    </label>
    <p class="hint">Hold Shift to move or turn freely</p>
  </div>
</template>
```

> **Try it:** drag the red rectangle until its edge lines up with the blue one, then turn it with
> the round handle.

## Turn and group

```ts
annotation.selection.rotateBy(90); // a quarter turn clockwise; -90 the other way
annotation.selection.resetRotation(); // upright again
```

Grouping makes several annotations select and move as one. The group is saved in the PDF the way
Acrobat saves it, so it stays a group there too:

```ts
if (annotation.selection.canGroup()) await annotation.selection.group();
if (annotation.selection.canUngroup()) await annotation.selection.ungroup();
```

**`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 AddShapes from './AddShapes.vue';
import TurnAndGroup from './TurnAndGroup.vue';

import '../turn-group.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>
      <AddShapes />
      <TurnAndGroup />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <AnnotationLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`turn-group.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;
}

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

**`AddShapes.vue`**

```vue
<script setup lang="ts">
import { computed, watch } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import { useAnnotation, useAnnotationState } from '@embedpdf/vue/annotation';

// On load: a rectangle and a circle on the cover, both selected.
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;
    void Promise.all([
      annotation.create(page, {
        subtype: 'square',
        box: { x: 300, y: 560, width: 140, height: 60 },
        color: '#e5484d',
        interiorColor: '#ffe4e1',
        strokeWidth: 3,
      }),
      annotation.create(page, {
        subtype: 'circle',
        box: { x: 460, y: 550, width: 80, height: 80 },
        color: '#1e90ff',
        strokeWidth: 3,
      }),
    ]).then((created) => annotation.selection.set(created.map((c) => c.annotation.ref)));
  },
  { immediate: true },
);
</script>

<template></template>
```

**`TurnAndGroup.vue`**

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

const annotation = useAnnotation();
// The template reads `selected`, so it draws again on every selection change:
// the buttons follow what's selected.
const { selected } = useAnnotationState();
</script>

<template>
  <div class="toolbar">
    <button
      type="button"
      class="button"
      :disabled="selected.length === 0"
      @click="annotation.selection.rotateBy(-90)"
    >
      ↺ Turn left
    </button>
    <button
      type="button"
      class="button"
      :disabled="selected.length === 0"
      @click="annotation.selection.rotateBy(90)"
    >
      ↻ Turn right
    </button>
    <button
      type="button"
      class="button"
      :disabled="selected.length === 0"
      @click="annotation.selection.resetRotation()"
    >
      Upright
    </button>
    <span class="spacer" />
    <button
      type="button"
      class="button"
      :disabled="!annotation.selection.canGroup()"
      @click="annotation.selection.group()"
    >
      Group
    </button>
    <button
      type="button"
      class="button"
      :disabled="!annotation.selection.canUngroup()"
      @click="annotation.selection.ungroup()"
    >
      Ungroup
    </button>
  </div>
</template>
```

> **Try it:** **Group** the two shapes, click empty space, then click one: both are selected.

To bring an annotation to the front or send it to the back, see
[Change the drawing order](with-code#change-the-drawing-order).

## Edit text

A double-click on a text box starts typing in it; a click outside ends it. From code:

```ts
annotation.text.begin(ref);
await annotation.text.toggleFormat('bold'); // the selected words, or the whole box
await annotation.text.end();
```

`annotation.text.getEditing()` is the box being typed in, or `null`. How fonts and formatting work
is on [Rich text](../concepts/rich-text).

## What people may change

`editable` says which annotations people may change by hand. One it leaves out can still be
selected, hovered, read and commented on, but it has no handles, doesn't move, turn or take typing,
the style panel has nothing for it, and `selection.update()` and `selection.delete()` leave it as
it is:

```ts
annotationPlugin({ editable: ['highlight', 'ink'] }); // only highlights and drawings change

annotation.updateSettings({ editable: false }); // nothing changes by hand, while the app runs
annotation.updateSettings({ editable: true }); // everything again, the default
```

The list names kinds by their `subtype`. A measurement goes by its `intent` instead
(`'line-dimension'`, `'polyline-dimension'` or `'polygon-dimension'`), so `['line']` keeps
measurements still and `['line-dimension']` lets only them change. `annotation.isEditable(ref)` says
whether people may change one.

It's only about people: your code still changes any annotation with `update()` and `delete()`, and
`canUpdate(ref)` and `canDelete(ref)` stay the same. To stop your code too, leave the permission out
of the [scope](../concepts/permissions).

## Shapes with many points

A polygon or a polyline takes a click per point, and a double-click ends it. While it's being drawn,
`<AnnotationDraftMenu>` can show buttons to finish or cancel it, which helps on touch screens:

**`draft-menu.vue`**

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

const annotation = useAnnotation();
</script>

<template>
  <AnnotationDraftMenu v-slot="{ draft }">
    <button :disabled="!draft.canFinish" @click="annotation.draft.finish()">Done</button>
    <button @click="annotation.draft.cancel()">Cancel</button>
  </AnnotationDraftMenu>
</template>
```

## Keyboard

The headless plugin doesn't take over any keys, since your app may already use them. Delete and
Escape are a few lines:

**`keyboard.vue`**

```vue
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
import { useAnnotation } from '@embedpdf/vue/annotation';
import { useInteraction } from '@embedpdf/vue/interaction';

const annotation = useAnnotation();
const interaction = useInteraction();

function onKey(event: KeyboardEvent) {
  if (event.target instanceof HTMLInputElement || annotation.text.getEditing()) return;
  if (event.key === 'Delete' || event.key === 'Backspace') void annotation.selection.delete();
  if (event.key === 'Escape') {
    annotation.cancel(); // a drag or a polygon in progress
    interaction.activateDefaultTool();
  }
}

onMounted(() => window.addEventListener('keydown', onKey));
onUnmounted(() => window.removeEventListener('keydown', onKey));
</script>

<template>
  <slot />
</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 AnnotationKeys from './AnnotationKeys.vue';
import DraftMenu from './DraftMenu.vue';
import Toolbar from './Toolbar.vue';

import '../polygon.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>
      <AnnotationKeys />
      <Toolbar />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <AnnotationLayer />
        </template>
        <template #overlay><DraftMenu /></template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`polygon.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;
}

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

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

/* The menu next to the selection */
.menu {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  padding: 4px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: 0 10px 28px -10px rgb(7 32 76 / 0.3);
}

.menu button {
  font: inherit;
  font-size: 12.5px;
  font-weight: 550;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-radius: 7px;
  padding: 6px 10px;
  cursor: pointer;
  white-space: nowrap;
}

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

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

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

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

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

**`AnnotationKeys.vue`**

```vue
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
import { useInteraction } from '@embedpdf/vue/interaction';
import { useAnnotation } from '@embedpdf/vue/annotation';

// Delete and Escape: the plugin leaves the keys to your app.
const annotation = useAnnotation();
const interaction = useInteraction();

function onKey(event: KeyboardEvent) {
  if (event.target instanceof HTMLInputElement || annotation.text.getEditing()) return;
  if (event.key === 'Delete' || event.key === 'Backspace') void annotation.selection.delete();
  if (event.key === 'Escape') {
    annotation.cancel(); // a drag or a polygon in progress
    interaction.activateDefaultTool();
  }
}

onMounted(() => window.addEventListener('keydown', onKey));
onUnmounted(() => window.removeEventListener('keydown', onKey));
</script>

<template></template>
```

**`DraftMenu.vue`**

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

// Done and Cancel next to the shape being drawn, for touch screens.
const annotation = useAnnotation();
</script>

<template>
  <AnnotationDraftMenu v-slot="{ draft }">
    <div class="menu" role="toolbar" aria-label="Shape">
      <button type="button" :disabled="!draft.canFinish" @click="annotation.draft.finish()">
        Done
      </button>
      <button type="button" @click="annotation.draft.cancel()">Cancel</button>
    </div>
  </AnnotationDraftMenu>
</template>
```

**`Toolbar.vue`**

```vue
<script setup lang="ts">
import { onMounted } from 'vue';
import { useInteraction, useInteractionState } from '@embedpdf/vue/interaction';

const TOOLS = [
  { id: 'pointer', label: 'Select' },
  { id: 'polygon', label: 'Polygon' },
  { id: 'polyline', label: 'Polyline' },
];

const interaction = useInteraction();
const { activeToolId } = useInteractionState();

// The polygon tool is active on load: click a few points on the page.
onMounted(() => interaction.activateTool('polygon'));
</script>

<template>
  <div class="toolbar">
    <div class="segmented" role="group" aria-label="Tool">
      <button
        v-for="tool in TOOLS"
        :key="tool.id"
        type="button"
        :aria-pressed="activeToolId === tool.id"
        @click="interaction.activateTool(tool.id)"
      >
        {{ tool.label }}
      </button>
    </div>
    <p class="hint">Delete removes the selection; Escape stops a shape</p>
  </div>
</template>
```

> **Try it:** click a few points on the page, then **Done**, or double-click. Select a shape and
> press Delete; Escape stops a shape you're drawing.

## Methods

What `useAnnotation()` gives you for selecting, drawing and editing text: its `selection`, `draft` and `text` parts. The rest is on
[Annotations in code](with-code#methods).

| Method                                                     | What it does                                                                                                                                                                                                   |
| :--------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `selection.set(refs)`, `selection.add(refs)`               | Selects these annotations, or adds them to the selection. An annotation in a group brings its group. [Select with code](#select-with-code)                                                                     |
| `selection.selectAll(page?)`                               | Selects everything that can be selected, on one page or in the whole document                                                                                                                                  |
| `selection.selectInRect(page, rect)`                       | Selects everything a box on a page touches of what it paints, with the rest of each one's group, as dragging on empty space does                                                                               |
| `selection.clear()`                                        | Clears the selection                                                                                                                                                                                           |
| `selection.update(changes)`                                | Changes the selected annotations: each takes the fields its kind has. Pass a function to change each one relative to itself. [A style panel](#a-style-panel)                                                   |
| `selection.updateLink(target)`                             | Links the selected annotations to a page or a website, or removes their links with `null`. [A style panel](#a-style-panel)                                                                                     |
| `selection.getProperties()`                                | What a style panel shows for the selection: `{ properties, values, mixed }`. In a component, use `useAnnotationProperties()`.                                                                                  |
| `selection.delete()`                                       | Deletes every selected annotation                                                                                                                                                                              |
| `selection.rotateBy(degrees)`, `selection.resetRotation()` | Turns the selection a quarter turn (`90` clockwise, `-90` the other way), or back upright. [Turn and group](#turn-and-group)                                                                                   |
| `selection.group()`, `selection.ungroup()`                 | Makes the selected annotations one group, saved the way Acrobat saves it, or splits the selected group up again                                                                                                |
| `selection.canGroup()`, `selection.canUngroup()`           | Whether the selection can be grouped, or is a group that can be split                                                                                                                                          |
| `selection.getAnchor()`                                    | Where a menu attaches: `{ page, bounds }` around the selection, or `null`. `<AnnotationMenu>` uses it. [Add a menu](#add-a-menu)                                                                               |
| `selection.getRotationAnchor()`                            | While a selection is being turned: `{ page, at, angle }`, the pointer's point and the angle so far in degrees clockwise, or `null`. `<AnnotationRotationBadge>` uses it to show the angle next to the pointer. |
| `draft.get()`                                              | The polygon or polyline being drawn (`{ page, bounds, pointCount, canFinish }`), or `null`. `<AnnotationDraftMenu>` uses it. [Shapes with many points](#shapes-with-many-points)                               |
| `draft.finish()`, `draft.cancel()`                         | Ends the shape and creates it, as a double-click does, or drops it                                                                                                                                             |
| `text.begin(ref)`, `text.end()`                            | Starts typing in a text box, as a double-click does, or stops and saves what was typed. [Edit text](#edit-text)                                                                                                |
| `text.toggleFormat(format)`                                | Turns `'bold'`, `'italic'` or `'underline'` on or off, for the selected words or the whole box                                                                                                                 |
| `isEditable(ref)`                                          | Whether people may change this annotation by hand, as the `editable` setting says. [What people may change](#what-people-may-change)                                                                           |
| `cancel()`                                                 | Stops a drag, or a polygon in progress. [Keyboard](#keyboard)                                                                                                                                                  |

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

## State

Everything `useAnnotationState()` gives you, as refs: your template updates when a value changes. The method in the last column reads the same value once.

| Field      | What it is                                                                                                | Read once           |
| :--------- | :-------------------------------------------------------------------------------------------------------- | :------------------ |
| `status`   | `'loading'` until every annotation is in, then `'ready'`; `'forbidden'` or `'error'` when they can't load | `getStatus()`       |
| `selected` | The selected annotations                                                                                  | `selection.list()`  |
| `hovered`  | The annotation under the pointer, or `null`                                                               | `getHovered()`      |
| `editing`  | The text box being typed in, or `null`                                                                    | `text.getEditing()` |

The annotations themselves come from `useAnnotationList(filter)`, a tool's defaults from
`useAnnotationDefaults(id)`, and a style panel's properties from `useAnnotationProperties()`.

## Events

**`events.vue`**

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

const sidebarOpen = ref(false);

useAnnotationEvent(
  (annotation) => annotation.onSelectionChanged,
  ({ refs }) => (sidebarOpen.value = refs.length > 0),
);
</script>

<template>
  <aside v-if="sidebarOpen"><!-- … --></aside>
</template>
```

| Event                | Fires when                                      | Carries                |
| :------------------- | :---------------------------------------------- | :--------------------- |
| `onSelectionChanged` | The selection changed                           | `refs`, `previousRefs` |
| `onDraftChanged`     | A polygon or polyline is being drawn, or ended  | `draft`                |
| `onEditingChanged`   | Typing in a text box started or ended           | `ref`                  |
| `onHoverChanged`     | The pointer moved onto an annotation, or off it | `ref`                  |

Changes to the annotations themselves are on [Events, saving & sharing](saving#events).

## Good to know

### Locked annotations

People can select a `locked` annotation, but not move, resize, turn or delete it, and
`lockedContents` stops them from changing its text. A `hidden` annotation can't be selected at all.
The `editable` setting does what `locked` does without writing the flag into the PDF: see
[What people may change](#what-people-may-change).

### Touch

A tap selects. The handles of a selected annotation are easier to grab with a finger: their grab
area is twice as large on touch. One finger on the selected annotation moves it; anywhere else, it
scrolls the page.
