Editable JavaScript Text Highlights – HighlightHelper.js

Category: Javascript , Text | October 26, 2026
Authorsamuelbradshaw
Last UpdateOctober 26, 2026
LicenseMIT
Views0 views
Editable JavaScript Text Highlights – HighlightHelper.js

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 mark rendering 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

ModeRendering Behavior
svg (default)Draws background shapes and overlapping highlights. Switches to mark elements for printing.
highlight-apiUses the CSS Custom Highlight API. Text styling is limited by that API and browser behavior.
mark-elementsCreates 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

OptionType, Default, and Description
drawingModeString. Default: svg. Accepted: svg, highlight-api, mark-elements. Selects the rendering mode.
snapToWordBoolean. Default: false. Snaps selections to word boundaries determined by spaces and dashes. Language-dependent.
longPressTimeoutInteger, milliseconds. Default: 500. Threshold used for isLongPress in hh:tap.
enableHoverBoolean. Default: false. Enables hh:hover events.
dragHandlesObject containing left and right HTML templates for custom handles. Defaults to the library’s built-in handle markup.
showDragHandlesArray. Default: ['highlights']. Accepted values: highlights, selection, touch. Controls when custom handles appear.
colorDefsObject mapping color names to CSS color values. Default names: red, orange, yellow, green, blue.
styleDefsObject mapping style names to style templates. Default names: fill, underline.
wrapperDefsObject mapping wrapper names to start/end HTML templates. Default name: screen-reader-label.

Default Colors

ColorDefault CSS Value
redhsl(360, 100%, 70%)
orangehsl(30, 100%, 60%)
yellowhsl(50, 100%, 60%)
greenhsl(80, 100%, 45%)
bluehsl(180, 100%, 45%)

Style and Wrapper Template Properties

Each entry in styleDefs accepts these properties:

PropertyDescription
cssRequired CSS declaration string for highlight-api, mark-elements, selection styling, and fallback rendering.
cssActiveCSS declarations for the active highlight. Falls back to css.
svgSVG markup for the SVG rendering mode. Falls back to css.
svgActiveSVG 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.

PlaceholderValue
{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

PropertyType, Default, and Description
highlightIdString. Unique highlight identifier, generated as hh-[timestamp] when omitted.
colorString. Key in colorDefs. Default: yellow.
styleString. Key in styleDefs. Default: fill.
wrapperString. Key in wrapperDefs, or null by default.
variablesObject of substitutions for templates. Default: null.
readOnlyBoolean. Default: false. Prevents editing of the individual highlight.
startParagraphIdString. ID of the first paragraph in the range.
startParagraphOffsetInteger. Inclusive character offset from the beginning of the first paragraph.
endParagraphIdString. ID of the last paragraph in the range.
endParagraphOffsetInteger. 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.

PropertyDescription
rangeTextPlain text inside the highlight.
rangeHtmlHTML inside the highlight.
rangeRectBounding DOMRect for the complete range.
rangeLineRectsArray of line-level DOMRect objects.
rangeParagraphIdsArray of paragraph IDs intersected by the highlight.
rangeObjBrowser Range object for the highlight.
resolvedDrawingModeRendering mode actually used, including a mark-elements fallback.
escapedHighlightIdCSS-escaped representation of the highlight identifier.

API Methods

MethodDescription
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

EventFired When
hh:tapThe reader taps or clicks within the annotatable region. Can identify intersected highlights, wrappers, and links.
hh:hoverPointer hover changes over a highlight, wrapper, or link. Requires enableHover: true.
hh:selectionchangeText selection or active-highlight state changes.
hh:highlightsloadA set of highlights is loaded.
hh:highlightcreateA highlight is created.
hh:highlightupdateA highlight is updated.
hh:highlightactivateA highlight becomes active.
hh:highlightdeactivateThe active highlight is deactivated.
hh:highlightremoveA highlight is removed.
hh:layoutchangeHighlights redraw after a container size change. detail includes width and height.

hh:tap details include targetCount, highlights, wrappers, hyperlinks, isLongPress, and selectionType. The hh:highlightsload event reports addedCount, removedCount, updatedCount, totalCount, and loadTimeMs.

container.addEventListener('hh:selectionchange', (event) => {
  console.log(highlighter.getSelectionState());
  console.log(event.detail);
});

CSS Styling Hooks and Element Attributes

Structural Attributes

AttributeElement
data-hh-containerAnnotatable root container.
data-hh-additionsContainer for drag handles and the SVG highlight background.
data-hh-handleParent for a drag handle.
data-hh-handle-contentChild element inside a drag handle.
data-hh-default-handleBuilt-in drag handle, replaceable with dragHandles.
data-hh-svg-backgroundSVG container for drawn background highlights.
data-hh-svg-active-overlaySVG group containing the active highlight shapes.
data-hh-wrapperElement for a highlight wrapper; its value identifies the wrapper type.

Identification and State Attributes

AttributeMeaning
data-hh-highlight-idHighlight ID on a mark or wrapper element.
data-hh-colorColor key on highlight and wrapper elements.
data-hh-styleStyle key on highlight and wrapper elements.
data-hh-positionHighlight or wrapper position: start, end, or start end.
data-hh-indexZero-based index of a hyperlink in the container.
data-hh-sideDrag handle side: left or right.
data-hh-wrapper-hashInternal marker for wrapper replacement.
data-pointer-downInternal pointer state on the container.
data-hh-ignoreExcludes the element from character-offset and range-rectangle calculations.

Other Styling and Access Hooks

  • .sr-only class: Provides screen-reader-only label text in wrappers.
  • --hh-color CSS custom property: Contains the current highlight color for style and drag-handle templates.
  • highlighter container 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

You Might Be Interested In:


Leave a Reply