
HighlightHelper.js is a vanilla JavaScript library for highlighting and underlining text on web pages.
It handles overlapping highlights, selections that cross paragraph boundaries, and editable ranges with drag handles.
Features
- Text highlights across inline elements and multiple paragraphs.
- Overlapping ranges and editable or read-only highlights.
- Fill and underline styles with custom color definitions.
- SVG, CSS Custom Highlight API, and HTML
markrendering modes. - Custom drag handles for editing selected ranges.
- Start and end wrappers for labels or annotation indicators.
- Tap, hover, selection, and highlight lifecycle events.
- Programmatic loading, updates, removal, and redraws.
How To Use It
Installation
Load the core JavaScript file from a CDN.
<script src="https://cdn.jsdelivr.net/gh/samuelbradshaw/highlight-helper-js@main/highlight-helper.min.js"></script>
Or install the npm package and import its Highlighter export.
npm install @samuelbradshaw/highlight-helper-js
import { Highlighter } from '@samuelbradshaw/highlight-helper-js';
For browser modules, import the module build directly.
import { Highlighter } from 'https://cdn.jsdelivr.net/gh/samuelbradshaw/highlight-helper-js@main/highlight-helper.min.mjs';
Basic Usage
Give each annotatable paragraph a unique id. The paragraph text and its character offsets determine where a saved highlight appears. This example loads a yellow highlight over the opening sentence.
<main id="reading-area">
<p id="intro-line">Highlight important passages in this document.</p>
<p id="second-line">Readers can also select other text.</p>
</main>
<script src="https://cdn.jsdelivr.net/gh/samuelbradshaw/highlight-helper-js@main/highlight-helper.min.js"></script>
<script>
const highlighter = new Highlighter('#reading-area', 'p[id]');
const firstParagraph = document.getElementById('intro-line');
highlighter.loadHighlights([{
highlightId: 'note-1',
color: 'yellow',
style: 'fill',
startParagraphId: 'intro-line',
startParagraphOffset: 0,
endParagraphId: 'intro-line',
endParagraphOffset: firstParagraph.textContent.length,
}]);
</script>
Create a Highlight from Selected Text
Call createOrUpdateHighlight() after your reader selects text inside the annotatable container. Place the button outside that container and prevent its mouse press from clearing the selection. This example continues from Basic Usage. crypto.randomUUID() requires a secure context.
<button type="button" id="highlight-selection">Highlight selection</button>
<script>
const button = document.getElementById('highlight-selection');
button.addEventListener('mousedown', (event) => event.preventDefault());
button.addEventListener('click', () => {
const selection = window.getSelection();
if (!selection || selection.isCollapsed || !selection.rangeCount) return;
const range = selection.getRangeAt(0);
const area = document.getElementById('reading-area');
if (!area.contains(range.startContainer) || !area.contains(range.endContainer)) return;
highlighter.createOrUpdateHighlight({
highlightId: crypto.randomUUID(),
color: 'green',
style: 'underline',
});
});
</script>
Activate Highlights on Tap
Attach interaction listeners to the annotatable container. A tap can intersect multiple highlights or hyperlinks. This handler activates a highlight only when it is the sole item at that point. For overlapping items, let your reader choose before calling activateHighlight() or activateHyperlink().
const container = document.getElementById('reading-area');
container.addEventListener('hh:tap', (event) => {
const detail = event.detail;
if (detail.isLongPress || detail.selectionType === 'Range') return;
if (detail.targetCount === 1 && detail.highlights.length === 1) {
highlighter.activateHighlight(detail.highlights[0].highlightId);
}
});
Restore and Save Highlight Data
HighlightHelper.js does not persist highlights. Store the editable fields in local storage or on a backend, then load them after the original text exists. Saved offsets depend on stable paragraph IDs and text content.
For a local-storage implementation, replace the demonstration highlighter.loadHighlights([...]) call in Basic Usage with the following code after constructing highlighter.
const readingContainer = document.getElementById('reading-area');
const STORAGE_KEY = 'reading-area-highlights';
const editableFields = [
'highlightId', 'color', 'style', 'wrapper', 'variables', 'readOnly',
'startParagraphId', 'startParagraphOffset',
'endParagraphId', 'endParagraphOffset',
];
let isRestoring = true;
function saveHighlights() {
if (isRestoring) return;
const records = highlighter.getHighlightInfo().map((highlight) =>
Object.fromEntries(editableFields.map((key) => [key, highlight[key]]))
);
localStorage.setItem(STORAGE_KEY, JSON.stringify(records));
}
for (const eventName of [
'hh:highlightcreate', 'hh:highlightupdate', 'hh:highlightremove',
]) {
readingContainer.addEventListener(eventName, saveHighlights);
}
let saved = [];
try {
const parsed = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
if (Array.isArray(parsed)) saved = parsed;
} catch {
// Ignore invalid stored JSON.
}
readingContainer.addEventListener('hh:highlightsload', () => {
isRestoring = false;
}, { once: true });
highlighter.loadHighlights(saved);
Choose a Drawing Mode
| Mode | Rendering Behavior |
|---|---|
svg (default) | Draws background shapes and overlapping highlights. Switches to mark elements for printing. |
highlight-api | Uses the CSS Custom Highlight API. Text styling is limited by that API and browser behavior. |
mark-elements | Creates HTML marks with CSS text styling and print output. Overlapping marks can affect screen-reader output. |
highlighter.setOptions({
drawingMode: 'mark-elements',
enableHover: true,
snapToWord: true,
colorDefs: {
purple: '#b197fc',
},
});
Customize Highlight Styles
You can pass templates through styleDefs to customize highlight styles. css is required for each definition. This example creates a custom fill over the opening word of second-line from Basic Usage. SVG templates can use placeholders such as {line.x} and {line.width} for each text line.
highlighter.setOptions({
colorDefs: { purple: '#b197fc' },
styleDefs: {
softFill: {
css: 'background-color: color-mix(in srgb, var(--hh-color) 45%, transparent);',
cssActive: 'background-color: var(--hh-color);',
},
},
});
highlighter.createOrUpdateHighlight({
highlightId: 'purple-note',
color: 'purple',
style: 'softFill',
startParagraphId: 'second-line',
startParagraphOffset: 0,
endParagraphId: 'second-line',
endParagraphOffset: 7,
}, true, false);
All Configuration Options
| Option | Type, Default, and Description |
|---|---|
drawingMode | String. Default: svg. Accepted: svg, highlight-api, mark-elements. Selects the rendering mode. |
snapToWord | Boolean. Default: false. Snaps selections to word boundaries determined by spaces and dashes. Language-dependent. |
longPressTimeout | Integer, milliseconds. Default: 500. Threshold used for isLongPress in hh:tap. |
enableHover | Boolean. Default: false. Enables hh:hover events. |
dragHandles | Object containing left and right HTML templates for custom handles. Defaults to the library’s built-in handle markup. |
showDragHandles | Array. Default: ['highlights']. Accepted values: highlights, selection, touch. Controls when custom handles appear. |
colorDefs | Object mapping color names to CSS color values. Default names: red, orange, yellow, green, blue. |
styleDefs | Object mapping style names to style templates. Default names: fill, underline. |
wrapperDefs | Object mapping wrapper names to start/end HTML templates. Default name: screen-reader-label. |
Default Colors
| Color | Default CSS Value |
|---|---|
red | hsl(360, 100%, 70%) |
orange | hsl(30, 100%, 60%) |
yellow | hsl(50, 100%, 60%) |
green | hsl(80, 100%, 45%) |
blue | hsl(180, 100%, 45%) |
Style and Wrapper Template Properties
Each entry in styleDefs accepts these properties:
| Property | Description |
|---|---|
css | Required CSS declaration string for highlight-api, mark-elements, selection styling, and fallback rendering. |
cssActive | CSS declarations for the active highlight. Falls back to css. |
svg | SVG markup for the SVG rendering mode. Falls back to css. |
svgActive | SVG markup for the active highlight. Falls back to svg, cssActive, or css. |
Each wrapperDefs entry accepts start and end HTML templates. Both default to null when absent. Style and wrapper templates accept var(--hh-color) and the substitutions below.
| Placeholder | Value |
|---|---|
{key} | Corresponding value from the highlight’s variables object. |
{line.x}, {line.y}, {line.width}, {line.height}, {line.top}, {line.bottom}, {line.left}, {line.right} | Rectangle dimensions and coordinates for the current line. |
{range.x}, {range.y}, {range.width}, {range.height}, {range.top}, {range.bottom}, {range.left}, {range.right} | Rectangle information for the highlighted range. |
{column.x}, {column.y}, {column.width}, {column.height}, {column.top}, {column.bottom}, {column.left}, {column.right} | Rectangle information for the closest column or block ancestor. |
{container.x}, {container.y}, {container.width}, {container.height}, {container.top}, {container.bottom}, {container.left}, {container.right} | Rectangle information for the annotatable container. |
Add a Start and End Label
The built-in screen-reader-label wrapper inserts text for screen readers. Give the highlight an explicit range or create it from an existing text selection. The label text comes from variables. The example uses the first paragraph from Basic Usage.
highlighter.createOrUpdateHighlight({
highlightId: 'accessible-note',
startParagraphId: 'intro-line',
startParagraphOffset: 0,
endParagraphId: 'intro-line',
endParagraphOffset: 9,
wrapper: 'screen-reader-label',
variables: {
startLabel: 'Start of an important passage',
endLabel: 'End of the important passage',
},
}, true, false);
Highlight Object Reference
loadHighlights() accepts an array of highlight objects. createOrUpdateHighlight() accepts an object containing only the properties to create or change. New and active highlights can use the current text selection for their range. Existing inactive highlights retain their range until new positions are provided.
Editable Properties
| Property | Type, Default, and Description |
|---|---|
highlightId | String. Unique highlight identifier, generated as hh-[timestamp] when omitted. |
color | String. Key in colorDefs. Default: yellow. |
style | String. Key in styleDefs. Default: fill. |
wrapper | String. Key in wrapperDefs, or null by default. |
variables | Object of substitutions for templates. Default: null. |
readOnly | Boolean. Default: false. Prevents editing of the individual highlight. |
startParagraphId | String. ID of the first paragraph in the range. |
startParagraphOffset | Integer. Inclusive character offset from the beginning of the first paragraph. |
endParagraphId | String. ID of the last paragraph in the range. |
endParagraphOffset | Integer. Exclusive character offset in the last paragraph. |
Computed Properties
These properties are available in highlight information returned by the library. They should not be edited as input fields.
| Property | Description |
|---|---|
rangeText | Plain text inside the highlight. |
rangeHtml | HTML inside the highlight. |
rangeRect | Bounding DOMRect for the complete range. |
rangeLineRects | Array of line-level DOMRect objects. |
rangeParagraphIds | Array of paragraph IDs intersected by the highlight. |
rangeObj | Browser Range object for the highlight. |
resolvedDrawingMode | Rendering mode actually used, including a mark-elements fallback. |
escapedHighlightId | CSS-escaped representation of the highlight identifier. |
API Methods
| Method | Description |
|---|---|
loadHighlights(highlights) | Loads an array of highlight records. Compares against current records and replaces the loaded set. Invalid or absent properties use previous or default values. |
createOrUpdateHighlight(properties, draw = true, activate = true) | Creates a highlight or changes an existing one. Returns the updated highlight object. draw and activate control drawing and activation after the update. |
drawHighlights(highlightIds = all) | Draws or redraws the given ID array, or every highlight by default. |
activateHighlight(highlightId) | Activates a highlight for editing. Only one can be active because activation uses the browser selection. |
activateHyperlink(index) | Opens a link by its zero-based index in the container. Useful when a tap intersects highlights and links. |
deactivateHighlights(removeSelectionRanges = true, redraw = true) | Ends active editing. Clears the browser selection and redraws by default. |
getActiveHighlightId() | Returns the currently active highlight ID, if any. |
getHighlightInfo(highlightIds = all, paragraphId = null) | Returns position-sorted highlight records. Optional ID array limits results; paragraphId filters by starting paragraph. |
getTargetsAtPoint(clientX, clientY) | Returns highlights, wrappers, and hyperlinks at viewport coordinates. |
setOptions(optionsToUpdate) | Changes one or more configuration properties. |
getOptions() | Returns the current option values. |
getSelectionState() | Returns information about the current text selection and active highlight. |
removeHighlights(highlightIds = all) | Removes the specified ID array or every highlight by default. |
removeHighlighter() | Removes the instance and its highlights. Creating a new Highlighter for the same container also removes the previous instance. |
Custom Events
| Event | Fired When |
|---|---|
hh:tap | The reader taps or clicks within the annotatable region. Can identify intersected highlights, wrappers, and links. |
hh:hover | Pointer hover changes over a highlight, wrapper, or link. Requires enableHover: true. |
hh:selectionchange | Text selection or active-highlight state changes. |
hh:highlightsload | A set of highlights is loaded. |
hh:highlightcreate | A highlight is created. |
hh:highlightupdate | A highlight is updated. |
hh:highlightactivate | A highlight becomes active. |
hh:highlightdeactivate | The active highlight is deactivated. |
hh:highlightremove | A highlight is removed. |
hh:layoutchange | Highlights redraw after a container size change. detail includes width and height. |
hh:tapdetails includetargetCount,highlights,wrappers,hyperlinks,isLongPress, andselectionType. Thehh:highlightsloadevent reportsaddedCount,removedCount,updatedCount,totalCount, andloadTimeMs.
container.addEventListener('hh:selectionchange', (event) => {
console.log(highlighter.getSelectionState());
console.log(event.detail);
});
CSS Styling Hooks and Element Attributes
Structural Attributes
| Attribute | Element |
|---|---|
data-hh-container | Annotatable root container. |
data-hh-additions | Container for drag handles and the SVG highlight background. |
data-hh-handle | Parent for a drag handle. |
data-hh-handle-content | Child element inside a drag handle. |
data-hh-default-handle | Built-in drag handle, replaceable with dragHandles. |
data-hh-svg-background | SVG container for drawn background highlights. |
data-hh-svg-active-overlay | SVG group containing the active highlight shapes. |
data-hh-wrapper | Element for a highlight wrapper; its value identifies the wrapper type. |
Identification and State Attributes
| Attribute | Meaning |
|---|---|
data-hh-highlight-id | Highlight ID on a mark or wrapper element. |
data-hh-color | Color key on highlight and wrapper elements. |
data-hh-style | Style key on highlight and wrapper elements. |
data-hh-position | Highlight or wrapper position: start, end, or start end. |
data-hh-index | Zero-based index of a hyperlink in the container. |
data-hh-side | Drag handle side: left or right. |
data-hh-wrapper-hash | Internal marker for wrapper replacement. |
data-pointer-down | Internal pointer state on the container. |
data-hh-ignore | Excludes the element from character-offset and range-rectangle calculations. |
Other Styling and Access Hooks
.sr-onlyclass: Provides screen-reader-only label text in wrappers.--hh-colorCSS custom property: Contains the current highlight color for style and drag-handle templates.highlightercontainer property: Exposes the associated Highlighter instance in JavaScript.
Style HTML Mark Highlights
These CSS rules round the edges of filled highlights drawn with HTML mark elements.
mark[data-hh-highlight-id][data-hh-style="fill"] {
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
}
mark[data-hh-highlight-id][data-hh-style="fill"][data-hh-position~="start"] {
border-radius: 0.2em 0 0 0.2em;
}
mark[data-hh-highlight-id][data-hh-style="fill"][data-hh-position~="end"] {
border-radius: 0 0.2em 0.2em 0;
}
Alternatives And Related Resources
- penciled: Persistent Text Highlights and Notes in JavaScript
- Interactive Text Annotation and Highlighting with Recogito Text Annotator
- Text Annotation And Highlighting Library – Annotate.js
- Animated Handdrawn Text Annotation Library – Rough Notation







