Annotation Plugin
The Annotation Plugin provides a comprehensive framework for adding, editing, and managing annotations within a PDF document. It supports a wide range of common annotation types, including text markups (highlight, underline), free-hand drawings (ink), shapes (squares, circles), and more.
The plugin is built on an extensible “Tool” system, allowing you to define and customize different annotation behaviors and appearances.
Installation
This plugin has several dependencies that must be installed to handle user interactions and manage state. The history plugin is optional but recommended for undo/redo functionality.
npm install @embedpdf/plugin-annotation @embedpdf/plugin-interaction-manager @embedpdf/plugin-selection @embedpdf/plugin-historyRegistration
Import AnnotationPluginPackage and its dependencies. It is crucial to register the dependencies before the annotation plugin itself.
import { createPluginRegistration } from '@embedpdf/core'
import { EmbedPDF } from '@embedpdf/core/vue'
// ... other imports
import { InteractionManagerPluginPackage } from '@embedpdf/plugin-interaction-manager/vue'
import { SelectionPluginPackage } from '@embedpdf/plugin-selection/vue'
import { HistoryPluginPackage } from '@embedpdf/plugin-history/vue'
import { AnnotationPluginPackage } from '@embedpdf/plugin-annotation/vue'
const plugins = [
// ... other essential plugins
createPluginRegistration(DocumentManagerPluginPackage, { /* ... */ }),
createPluginRegistration(RenderPluginPackage),
// Register dependencies first
createPluginRegistration(InteractionManagerPluginPackage),
createPluginRegistration(SelectionPluginPackage),
createPluginRegistration(HistoryPluginPackage),
// Register and configure the annotation plugin
createPluginRegistration(AnnotationPluginPackage, {
// Optional: Set the author name for created annotations
annotationAuthor: 'Jane Doe',
}),
]Usage
The plugin works by combining a UI component to render the annotations with a composable to manage the annotation tools and state.
1. The <AnnotationLayer /> Component
This component is responsible for rendering all annotations and handling user interactions like creating, selecting, moving, and resizing. It must be placed inside the <Scroller />’s default slot and wrapped by a <PagePointerProvider> to correctly process pointer events.
Note that documentId is required for all components to link them to the correct document state.
<script setup lang="ts">
import { PagePointerProvider } from '@embedpdf/plugin-interaction-manager/vue';
import { AnnotationLayer } from '@embedpdf/plugin-annotation/vue';
</script>
<template>
<Scroller :document-id="activeDocumentId">
<template #default="{ page }">
<PagePointerProvider :document-id="activeDocumentId" :page-index="page.pageIndex">
<RenderLayer :document-id="activeDocumentId" :page-index="page.pageIndex" />
<SelectionLayer :document-id="activeDocumentId" :page-index="page.pageIndex" />
<AnnotationLayer
:document-id="activeDocumentId"
:page-index="page.pageIndex"
/>
</PagePointerProvider>
</template>
</Scroller>
</template>2. Building an Annotation Toolbar
The useAnnotation composable provides all the necessary methods to control the plugin for a specific document. You can build a toolbar that allows users to select an “active tool” (like a pen or highlighter) and perform actions like deleting a selected annotation.
<script setup lang="ts">
import { useAnnotation } from '@embedpdf/plugin-annotation/vue';
const props = defineProps<{ documentId: string }>();
const { provides: annotationApi, state } = useAnnotation(() => props.documentId);
const deleteSelected = () => {
const selection = annotationApi.value?.getSelectedAnnotation();
if (selection) {
annotationApi.value?.deleteAnnotation(selection.object.pageIndex, selection.object.id);
}
};
</script>
<template>
<div>
<button @click="annotationApi?.setActiveTool('highlight')">Highlighter</button>
<button @click="annotationApi?.setActiveTool('ink')">Pen</button>
<button @click="deleteSelected" :disabled="!state.selectedUid">Delete</button>
</div>
</template>3. Adding a Selection Menu
When an annotation is selected, you can display a contextual menu (e.g., for deleting, editing properties). Use the selection-menu scoped slot on <AnnotationLayer />. This slot receives props including the context object, which contains the selected annotation.
<script setup lang="ts">
import { AnnotationLayer, useAnnotation } from '@embedpdf/plugin-annotation/vue';
const props = defineProps<{ documentId: string }>();
const { provides: annotationApi } = useAnnotation(() => props.documentId);
const handleDelete = (pageIndex: number, id: number) => {
annotationApi.value?.deleteAnnotation(pageIndex, id);
};
</script>
<template>
<AnnotationLayer :document-id="documentId" :page-index="page.pageIndex">
<template #selection-menu="{ selected, context, menuWrapperProps, rect }">
<div v-if="selected" v-bind="menuWrapperProps">
<div
:style="{
position: 'absolute',
top: `${rect.size.height + 8}px`,
pointerEvents: 'auto',
cursor: 'default'
}"
>
<button @click="handleDelete(context.annotation.object.pageIndex, context.annotation.object.id)">
Delete
</button>
</div>
</div>
</template>
</AnnotationLayer>
</template>The menuWrapperProps must be bound to your wrapper element—it handles proper positioning relative to page rotation.
4. Creating Custom Tools
You can extend the plugin’s functionality by adding your own tools. For example, you could create a custom image stamp. This is done by calling addTool on the plugin’s capability, often within the @initialized event handler of the <EmbedPDF> component.
<script setup lang="ts">
import type { PluginRegistry } from '@embedpdf/core';
import { AnnotationPlugin, type AnnotationTool } from '@embedpdf/plugin-annotation/vue';
import { PdfAnnotationSubtype, type PdfStampAnnoObject } from '@embedpdf/models';
const handleInitialized = (registry: PluginRegistry) => {
const annotationApi = registry.getPlugin<AnnotationPlugin>('annotation')?.provides();
annotationApi?.addTool<AnnotationTool<PdfStampAnnoObject>>({
id: 'stampApproved',
name: 'Approved Stamp',
interaction: { exclusive: false, cursor: 'copy' },
matchScore: () => 0, // Doesn't match existing annotations
defaults: {
type: PdfAnnotationSubtype.STAMP,
imageSrc: '/images/approved-stamp.png', // URL to your stamp image
},
});
};
</script>
<template>
<EmbedPDF :engine="engine" :plugins="plugins" @initialized="handleInitialized">
</EmbedPDF>
</template>Listening to Annotation Events
For more advanced integrations, such as saving annotation data to a backend or synchronizing state with an external data store, you can subscribe to annotation lifecycle events using onAnnotationEvent.
Each event provides the annotation object and a committed flag, which indicates whether the change has been saved to the underlying PDF document in the engine.
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
import { useAnnotation } from '@embedpdf/plugin-annotation/vue';
const props = defineProps<{ documentId: string }>();
const { provides: annotationApi } = useAnnotation(() => props.documentId);
let unsubscribe: (() => void) | undefined;
onMounted(() => {
if (!annotationApi.value) return;
unsubscribe = annotationApi.value.onAnnotationEvent((event) => {
console.log(`Annotation event: ${event.type}`, { event });
// Example: Save to backend after a change is committed to the engine
if (event.type === 'create' && event.committed) {
// yourApi.saveAnnotation(event.annotation);
}
});
});
onUnmounted(() => {
unsubscribe?.();
});
</script>Default Annotation Tools
The plugin comes with a set of pre-configured tools. You can activate any of these tools by passing its id to the setActiveTool method. You can also override their default properties by providing a modified AnnotationTool object with a matching id in the plugin configuration.
| Tool Name | id | Description |
|---|---|---|
| Highlight | highlight | Creates text highlight annotations. |
| Underline | underline | Creates text underline annotations. |
| Strikeout | strikeout | Creates text strikeout annotations. |
| Squiggly | squiggly | Creates squiggly text underline annotations. |
| Pen | ink | Free-hand drawing tool. |
| Ink Highlighter | inkHighlighter | Free-hand highlighter with a multiply blend mode. |
| Circle | circle | Draws ellipse annotations. |
| Square | square | Draws rectangle annotations. |
| Line | line | Draws straight line annotations. |
| Arrow | lineArrow | Draws a straight line with an arrowhead. |
| Polyline | polyline | Draws multi-segment lines. |
| Polygon | polygon | Draws closed, multi-sided shapes. |
| Free Text | freeText | Adds a text box annotation. |
| Image | stamp | Adds an image stamp. Opens a file picker by default. |
Live Example
The example below includes a toolbar for selecting different annotation tools (highlight, ink pen, square) and a delete button. When you select an annotation, a contextual menu appears with a delete option—demonstrating the selection-menu scoped slot.
Customizing Annotation UI
The <AnnotationLayer /> is fully headless — you can replace every visual element with your own markup. This includes resize handles, vertex handles, the rotation handle, and the selection outline.
In Vue, custom handles are provided via scoped slots rather than component props. Each slot receives positioning styles, event handlers, and appearance data that you must bind onto your element for interactions to work correctly.
Custom Handle Slots
#resize-handle
Replace the default circular resize handles with custom markup. The slot receives style, backgroundColor, and event handler props.
<AnnotationLayer :document-id="documentId" :page-index="page.pageIndex"
:resize-ui="{ size: 10, color: '#475569' }">
<template #resize-handle="{ style, backgroundColor, key: _key, ...rest }">
<div v-bind="rest" :style="{
...style,
backgroundColor: 'transparent',
border: `2px solid ${backgroundColor ?? '#475569'}`,
borderRadius: '2px',
}" />
</template>
</AnnotationLayer>#vertex-handle
Replace vertex handles (used on polyline/polygon annotations) with custom shapes. This example creates diamond-shaped handles by adding a rotate(45deg) transform.
<template #vertex-handle="{ style, backgroundColor, key: _key, ...rest }">
<div v-bind="rest" :style="{
...style,
backgroundColor: backgroundColor ?? '#475569',
borderRadius: '1px',
transform: `${style?.transform ?? ''} rotate(45deg)`.trim(),
}" />
</template>#rotation-handle
Replace the rotation handle with fully custom markup. The slot provides additional props for the connector line and icon color.
<template #rotation-handle="{ style, backgroundColor, connectorStyle, showConnector, iconColor, border: _border, ...rest }">
<!-- Connector line (optional) -->
<div v-if="showConnector && connectorStyle" :style="connectorStyle" />
<!-- Handle element -->
<div v-bind="rest" :style="{
...style,
backgroundColor: backgroundColor ?? '#475569',
borderRadius: '999px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 1px 3px rgba(0,0,0,0.25)',
}">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none"
:stroke="iconColor ?? 'white'" stroke-width="2.5"
stroke-linecap="round" stroke-linejoin="round">
<path d="M1 4v6h6" />
<path d="M23 20v-6h-6" />
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10M23 14l-4.64 4.36A9 9 0 0 1 3.51 15" />
</svg>
</div>
</template>Configuring Outline and Handle Props
Pass UI configuration alongside the slots to control sizes, colors, and outline styles:
<AnnotationLayer
:document-id="documentId"
:page-index="page.pageIndex"
:resize-ui="{ size: 10, color: '#475569' }"
:vertex-ui="{ size: 10, color: '#475569' }"
:rotation-ui="{
size: 24,
color: '#475569',
iconColor: 'white',
margin: 28,
showConnector: true,
connectorColor: '#94a3b8',
}"
:selection-outline="{ color: '#475569', style: 'solid', width: 1, offset: 2 }"
:group-selection-outline="{ color: '#64748b', style: 'dashed', width: 2, offset: 3 }"
>
<template #resize-handle="{ style, backgroundColor, key: _key, ...rest }">
<!-- your custom resize handle -->
</template>
<template #vertex-handle="{ style, backgroundColor, key: _key, ...rest }">
<!-- your custom vertex handle -->
</template>
<template #rotation-handle="{ style, backgroundColor, connectorStyle, showConnector, iconColor, border: _border, ...rest }">
<!-- your custom rotation handle -->
</template>
</AnnotationLayer>Live Example — Custom UI
Try creating annotations below to see the custom square resize handles, diamond vertex handles, and pill-shaped rotation handle in action.
API Reference
Configuration (AnnotationPluginConfig)
| Option | Type | Description |
|---|---|---|
annotationAuthor | string | Sets the author name for all created annotations. Default: 'Guest' |
autoCommit | boolean | If true, annotation changes are automatically saved to the engine. Default: true |
tools | AnnotationTool[] | An array of custom annotation tools to add or override default tools. |
colorPresets | string[] | A list of hex color strings to be used in a color picker UI. |
deactivateToolAfterCreate | boolean | If true, the active tool is deselected after an annotation is created. Default: false |
selectAfterCreate | boolean | If true, a newly created annotation is automatically selected. Default: true |
Component: <AnnotationLayer />
The primary component for rendering and interacting with annotations.
| Prop | Type | Description |
|---|---|---|
documentId | string | (Required) The ID of the document this layer belongs to. |
pageIndex | number | (Required) The page index this layer corresponds to. |
scale | number | The zoom scale of the page. |
rotation | number | The rotation of the page. |
resizeUi | ResizeHandleUI | Customize resize handle size and color. |
vertexUi | VertexHandleUI | Customize vertex handle size and color. |
rotationUi | RotationHandleUI | Customize rotation handle size, color, margin, icon, and connector. |
selectionOutline | SelectionOutline | Customize the outline around selected annotations (color, style, width, offset). |
groupSelectionOutline | SelectionOutline | Customize the outline for group selections. Falls back to selectionOutline. |
selectionOutlineColor | string | Deprecated. Use selectionOutline.color instead. |
ResizeHandleUI / VertexHandleUI
interface ResizeHandleUI {
size?: number; // Handle size in px (default: 12)
color?: string; // Background color (default: '#007ACC')
}
// VertexHandleUI has the same shapeRotationHandleUI
interface RotationHandleUI {
size?: number; // Handle size in px (default: 16)
color?: string; // Background color (default: 'white')
iconColor?: string; // Icon stroke color (default: '#007ACC')
margin?: number; // Gap from bounding box edge in px (default: 35)
showConnector?: boolean; // Show connector line (default: false)
connectorColor?: string; // Connector line color (default: '#007ACC')
border?: RotationHandleBorder; // Border configuration
}
interface RotationHandleBorder {
color?: string; // Border color (default: '#007ACC')
style?: BorderStyle; // Border style (default: 'solid')
width?: number; // Border width in px (default: 1)
}SelectionOutline
type BorderStyle = 'solid' | 'dashed' | 'dotted';
interface SelectionOutline {
color?: string; // Outline color (default: '#007ACC')
style?: BorderStyle; // Default: 'solid' (single) / 'dashed' (group)
width?: number; // Outline width in px (default: 1 / 2)
offset?: number; // Outline offset in px (default: 1 / 2)
}Scoped Slots
The <AnnotationLayer> provides scoped slots to render custom UI elements.
#selection-menu
Render a custom menu when an annotation is selected.
| Prop | Type | Description |
|---|---|---|
context | object | The selection context containing the annotation object. |
selected | boolean | true if an annotation is currently selected. |
menuWrapperProps | object | Vue props to bind to your menu’s wrapper element for positioning. |
rect | Rect | The bounding box of the selected annotation. |
#resize-handle
Replace the default resize handles with custom markup.
| Prop | Type | Description |
|---|---|---|
style | CSSProperties | Positioning and sizing styles. Must be applied to your element. |
backgroundColor | string | The configured handle color (from resizeUi.color). |
...rest | — | Event handlers and data attributes. Must be bound with v-bind. |
#vertex-handle
Replace the default vertex handles with custom markup.
| Prop | Type | Description |
|---|---|---|
style | CSSProperties | Positioning and sizing styles. Must be applied to your element. |
backgroundColor | string | The configured handle color (from vertexUi.color). |
...rest | — | Event handlers and data attributes. Must be bound with v-bind. |
#rotation-handle
Replace the default rotation handle with custom markup.
| Prop | Type | Description |
|---|---|---|
style | CSSProperties | Positioning and sizing styles. Must be applied to your element. |
backgroundColor | string | The configured handle color (from rotationUi.color). |
iconColor | string | The configured icon color (from rotationUi.iconColor). |
connectorStyle | CSSProperties | undefined | Style for the connector line, if enabled. |
showConnector | boolean | Whether the connector line should be rendered. |
border | RotationHandleBorder | Resolved border configuration (color, style, width). |
...rest | — | Event handlers and data attributes. Must be bound with v-bind. |
Composable: useAnnotation(documentId)
Connects your components to the annotation plugin’s state and methods for a specific document.
Parameters
| Parameter | Type | Description |
|---|---|---|
documentId | () => string | A getter function that returns the document ID to track. |
Returns
| Property | Type | Description |
|---|---|---|
provides | Ref<AnnotationScope | null> | A ref object with methods to control the plugin, or null if not ready. |
state | AnnotationState | Reactive state object containing properties like activeToolId, selectedUid. |
AnnotationScope Methods
A selection of key methods available on the provides.value object:
| Method | Description |
|---|---|
setActiveTool(toolId) | Activates an annotation tool (e.g., 'ink', 'highlight'). Pass null to deactivate. |
getActiveTool() | Returns the currently active AnnotationTool object, or null. |
addTool(tool) | Registers a new custom AnnotationTool. |
createAnnotation(..) | Programmatically creates a new annotation on a page. |
updateAnnotation(..) | Updates the properties of an existing annotation. |
deleteAnnotation(..) | Deletes an annotation from a page. |
selectAnnotation(..) | Programmatically selects an annotation. |
getSelectedAnnotation() | Returns the currently selected TrackedAnnotation object, or null. |
importAnnotations(..) | Imports an array of annotations into the viewer. |
commit() | Manually saves all pending annotation changes to the PDF document. |
onStateChange(cb) | Subscribes to any change in the annotation state. |
onAnnotationEvent(cb) | Subscribes to events like annotation creation, updates, or deletion. |
The AnnotationEvent Object
The object passed to the onAnnotationEvent callback contains details about the lifecycle event. Its shape varies based on the type property.
| Event Type | Properties | Description |
|---|---|---|
'create' | annotation, pageIndex, ctx, committed | Fired when an annotation is created. ctx may contain extra data like ImageData for stamps. committed is true if the change has been saved to the engine. |
'update' | annotation, pageIndex, patch, committed | Fired when an annotation is moved, resized, or its properties change. annotation is the original object, and patch contains only the changed properties. |
'delete' | annotation, pageIndex, committed | Fired when an annotation is deleted. |
'loaded' | total | Fired once when the initial set of annotations is loaded from the document. total is the number of annotations loaded. |
Need Help?
Join our community for support, discussions, and to contribute to EmbedPDF's development.