
Recogito Text Annotator is a vanilla JavaScript library that adds interactive text annotation and highlight functionality to any web page.
Each annotation stores a selected quote, character offsets, metadata bodies, and optional user details.
You can control annotation creation, styling, filtering, and data serialization through a clean API.
Features:
- Create interactive highlights from text selections.
- Load saved annotations from JSON endpoints.
- Store quote-based ranges with metadata bodies.
- Merge adjacent highlight segments.
- Filter visible annotations with custom rules.
- Track annotation creation, updates, deletion, and selection.
- Apply static or data-driven highlight styles.
- Manage undo and redo history.
Use Cases:
- Digital humanities researchers building a manuscript reader where scholars can annotate passages and export the data.
- Online course platforms that let students highlight and comment on reading material, then save those notes to a user profile.
- Collaborative editorial tools where reviewers tag text sections with status labels and discuss changes inline.
- Public-facing article pages that load community annotations from a database and render them as layered highlights.
- Compliance review systems where analysts select contract clauses and attach regulatory comments for auditing.
How To Use It
Installation
Install the Recogito Text Annotator package with npm.
npm install @recogito/text-annotator
Import the JavaScript module and its required stylesheet from your app entry file.
import { createTextAnnotator } from '@recogito/text-annotator';
import '@recogito/text-annotator/text-annotator.css';
Basic Usage
Place the text inside one stable container. Initialize the annotator after that element exists on the page.
<article id="release-notes">
<h2>Service Update</h2>
<p>
Archived project records remain available for seven years after account closure.
</p>
</article>
import { createTextAnnotator } from '@recogito/text-annotator';
import '@recogito/text-annotator/text-annotator.css';
// Select the text container that accepts annotations.
const releaseNotes = document.querySelector('#release-notes');
// Create the annotator instance.
const annotator = createTextAnnotator(releaseNotes, {
user: {
id: 'editor-204',
name: 'Maya Chen'
}
});
// Handle annotations created from text selections.
annotator.on('createAnnotation', (annotation) => {
console.log('New annotation:', annotation);
});
Advanced Usages
Load saved annotations from an API
This snippet replaces the current in-memory annotations with records returned from an endpoint.
// Wait for the saved annotation records before continuing.
await annotator.loadAnnotations('/api/release-notes/2026-06/annotations');
Control overlapping and adjacent selections
This configuration selects every intersecting annotation and merges nearby highlight fragments.
const annotator = createTextAnnotator(releaseNotes, {
allowModifierSelect: true,
selectionMode: 'all',
mergeHighlights: {
horizontalTolerance: 3,
verticalTolerance: 2
}
});
Style tags and comments differently
This style callback changes highlight color and hover opacity from annotation metadata.
annotator.setStyle((annotation, state) => {
const hasTag = annotation.bodies.some(
(body) => body.purpose === 'tagging'
);
return {
fill: hasTag ? '#fde68a' : '#bfdbfe',
fillOpacity: state.hovered ? 0.42 : 0.24,
underlineStyle: hasTag ? 'solid' : 'dashed',
underlineColor: hasTag ? '#b45309' : '#1d4ed8',
underlineThickness: 2
};
});
Extend an existing annotation range
This mode adds each new selection to the active annotation instead of creating another record.
// Extend the selected annotation with the next text selection.
annotator.setAnnotatingMode('ADD_TO_CURRENT');
Select and scroll to an annotation from another UI area
This handler updates the selection state and scrolls only when the target range is rendered.
const annotationList = document.querySelector('#annotation-list');
annotationList.addEventListener('click', (event) => {
const button = event.target.closest('[data-annotation-id]');
if (!button) return;
const annotationId = button.dataset.annotationId;
// Sync the current selection with the external control.
annotator.setSelected(annotationId);
// Scroll to the highlighted passage when it exists in the document.
annotator.scrollIntoView(annotationId);
});
All Configuration Options
allowModifierSelect(boolean): Lets users extend an existing annotation while holding Ctrl or Cmd. The default isfalse.annotatingEnabled(boolean): Enables or disables creation of new annotations. The default istrue.dismissOnNotAnnotatable('NEVER' | 'ALWAYS' | function): Controls whether the current selection clears after a click outside the annotatable area. The default is'NEVER'.mergeHighlights(object): Merges adjacent highlight fragments. SethorizontalToleranceandverticalTolerancein pixels.selectionMode('shortest' | 'all'): Defines which annotation receives selection when ranges overlap. The default is'shortest'.style(HighlightStyleExpression): Sets a static highlight style object or a function that returns styles from annotation data and interaction state.user(User): Defines the current user record for newly created and updated annotations.userSelectAction(UserSelectActionExpression): Defines what happens after a user selects an annotation. The default action is'SELECT'.
API Methods
// Create a reusable annotation record.
const annotationRecord = {
id: 'review-note-502',
bodies: [],
target: {
selector: [{
quote: 'Archived project records remain available for seven years',
start: 0,
end: 58
}]
}
};
// Add one annotation programmatically.
annotator.addAnnotation(annotationRecord);
// Cancel the current interactive selection.
annotator.cancelSelected();
// Check whether the latest action can be undone.
const undoAvailable = annotator.canUndo();
// Check whether an undone action can be restored.
const redoAvailable = annotator.canRedo();
// Remove every annotation from the current instance.
annotator.clearAnnotations();
// Destroy the instance and remove its event listeners.
annotator.destroy();
// Retrieve one annotation by its ID.
const selectedRecord = annotator.getAnnotationById('review-note-502');
// Return every annotation in the current instance.
const allAnnotations = annotator.getAnnotations();
// Return the currently selected annotations.
const activeAnnotations = annotator.getSelected();
// Return the current user record.
const currentUser = annotator.getUser();
// Load annotations from a URL and replace existing records.
await annotator.loadAnnotations('/api/release-notes/annotations');
// Undo the previous user edit.
annotator.undo();
// Redo the latest undone user edit.
annotator.redo();
// Remove an annotation by ID.
annotator.removeAnnotation('review-note-502');
// Remove an annotation by object reference.
annotator.removeAnnotation(annotationRecord);
// Scroll an annotation range into view.
const isRendered = annotator.scrollIntoView('review-note-502');
// Disable creation of new annotations.
annotator.setAnnotatingEnabled(false);
// Re-enable creation of new annotations.
annotator.setAnnotatingEnabled(true);
// Create a new annotation for each text selection.
annotator.setAnnotatingMode('CREATE_NEW');
// Extend the active annotation range.
annotator.setAnnotatingMode('ADD_TO_CURRENT');
// Replace the active annotation range.
annotator.setAnnotatingMode('REPLACE_CURRENT');
// Replace all existing records with a new collection.
annotator.setAnnotations([annotationRecord]);
// Append records without removing current annotations.
annotator.setAnnotations([annotationRecord], false);
// Show only annotations that contain a commenting body.
annotator.setFilter((annotation) =>
annotation.bodies.some((body) => body.purpose === 'commenting')
);
// Select one annotation.
annotator.setSelected('review-note-502');
// Select multiple annotations.
annotator.setSelected(['review-note-502', 'review-note-503']);
// Clear the current annotation selection.
annotator.setSelected();
// Apply one static highlight style.
annotator.setStyle({
fill: '#fef08a',
fillOpacity: 0.25
});
// Apply dynamic styles from annotation data and interaction state.
annotator.setStyle((annotation, state) => ({
fill: state.selected ? '#fdba74' : '#bfdbfe',
fillOpacity: state.hovered ? 0.4 : 0.22
}));
// Set the active user for new and updated records.
annotator.setUser({
id: 'reviewer-330',
name: 'Jordan Lee'
});
// Disable interactive selection of existing annotations.
annotator.setUserSelectAction('NONE');
// Select only annotations that contain metadata bodies.
annotator.setUserSelectAction((annotation) =>
annotation.bodies.length ? 'SELECT' : 'NONE'
);
// Hide every rendered annotation.
annotator.setVisible(false);
// Show every rendered annotation.
annotator.setVisible(true);
// Update a stored annotation record.
annotator.updateAnnotation({
...annotationRecord,
bodies: [{
purpose: 'commenting',
value: 'Confirm the retention period before publication.'
}]
});
Events
// Save a newly created annotation.
const saveNewAnnotation = async (annotation) => {
await fetch('/api/release-notes/annotations', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(annotation)
});
};
annotator.on('createAnnotation', saveNewAnnotation);
// Save an updated annotation and inspect its earlier version.
annotator.on('updateAnnotation', (annotation, previous) => {
console.log('Updated annotation:', annotation);
console.log('Previous annotation:', previous);
});
// Remove a deleted annotation from application storage.
annotator.on('deleteAnnotation', (annotation) => {
console.log('Deleted annotation:', annotation.id);
});
// Sync external controls after user selection changes.
annotator.on('selectionChanged', (annotations) => {
console.log('Selected annotations:', annotations);
});
// Track annotations that enter or leave the visible viewport.
annotator.on('viewportIntersect', (annotations) => {
console.log('Visible annotation ranges:', annotations);
});
// Remove a listener when the surrounding component unmounts.
annotator.off('createAnnotation', saveNewAnnotation);
Alternatives:
- Text Annotation And Highlighting Library – Annotate.js
- Underline/Highlight/Strike Text With The textAnnotator Library
- Animated Handdrawn Text Annotation Library – Rough Notation
- 10 Best Text Highlighting Plugins In JavaScript
FAQs:
Q: How do I save annotations to a backend?
A: Listen for createAnnotation, updateAnnotation, and deleteAnnotation. Send the annotation record to your API and load saved records with loadAnnotations() after the target text renders.
Q: Why do saved highlights appear in the wrong place?
A: The selected quote and character offsets must match the exact text in the annotated container. Reload or rebuild annotations after meaningful edits to the source text.
Q: How do I extend an existing annotation instead of creating another one?
A: Call setAnnotatingMode('ADD_TO_CURRENT') before the next text selection. You can also enable allowModifierSelect for Ctrl or Cmd-based range extension.
Q: Does the library come with a built-in annotation form or toolbar?
A: No, it only manages text selection and highlight rendering. You must build your own UI for creating annotation bodies (comments, tags) and use the API or createAnnotation event to add them to the store.
Q: Can I use Recogito Text Annotator with React or Vue?
A: Yes. A dedicated React wrapper (@recogito/text-annotator-react) provides a component-based integration. For Vue or other frameworks, you can mount the annotator on a DOM element inside a mounted lifecycle hook and manage state manually.







