English | 简体中文
The project began with a simple idea: highlight text on a website and save those highlights, much like you would in a PDF.
If you have visited medium.com, you may be familiar with its highlighting feature: users select a text segment and click Highlight. The selected text receives a highlighted background, and the highlight is saved and restored on later visits. The following GIF shows a simple demo.
This feature is useful for readers. Developers may want to add it to their websites, while users may want a browser extension that provides it.
web-highlighter helps you implement text highlighting and persistence on websites such as blogs, document viewers, and online books. It provides the core functionality and easy-to-use APIs for building your own product, and it is used in production.
This project uses Node.js 22.15.1 and publishes ES5 output with IE11 support. With nvm installed, run:
nvm use
npm ci
npm testnpm i web-highlighterAdd these two lines to automatically highlight selected text.
import Highlighter from 'web-highlighter';
new Highlighter().run();To persist highlights, add the following setup:
import Highlighter from 'web-highlighter';
// 1. Initialize the highlighter.
const highlighter = new Highlighter();
// 2. Retrieve persisted data from the backend, then restore highlights.
getRemoteData().then(source => highlighter.fromStore(
source.startMeta,
source.endMeta,
source.text,
source.id,
));
// 3. Save newly created highlights to the backend.
highlighter.on(Highlighter.event.CREATE, ({ sources }) => save(sources));
// 4. Enable automatic highlighting.
highlighter.run();A more complete integration:
import Highlighter from 'web-highlighter';
// Do not highlight text within <pre> or <code> elements.
const highlighter = new Highlighter({
exceptSelectors: ['pre', 'code'],
});
// Handle highlight interactions.
highlighter
.on(Highlighter.event.HOVER, ({ id }) => {
highlighter.addClass('highlight-wrap-hover', id);
})
.on(Highlighter.event.HOVER_OUT, ({ id }) => {
highlighter.removeClass('highlight-wrap-hover', id);
})
.on(Highlighter.event.CREATE, ({ sources }) => {
store.save(sources.map(hs => ({ hs })));
});
// Restore persisted highlights.
store.getAll().forEach(({ hs }) => {
highlighter.fromStore(hs.startMeta, hs.endMeta, hs.text, hs.id);
});
highlighter.run();This repository also includes an interactive example in the example directory:
npm ci
npm startThen visit http://127.0.0.1:8085/.
An example of a production product built with web-highlighter (the highlights appear on the left):
web-highlighter reads the current selection through the Selection API and converts it into a serializable HighlightSource. You can persist this data in a backend and restore the highlight when the user returns to the page. The data format is independent of the technology stack, so it works with pages built using React, Vue, Angular, jQuery, and other libraries.
Persisted highlights record DOM paths and text offsets relative to $root; they are not screen coordinates. Restore highlights only after content is fully rendered, using a compatible $root, document structure, text segmentation, and wrapTag configuration. If a framework re-renders the content or the document changes, persisted positions may no longer be valid. Use the Serialize.Restore hook to implement a restoration strategy for changing content.
A cross-paragraph selection creates a single continuous HighlightSource. If your application requires one persisted record per paragraph, split the selection into a separate Range for each paragraph before creating highlights. Call fromRange() for each range and persist each returned HighlightSource separately. Use the Serialize.RecordInfo hook to store paragraph-specific identifiers in source.extra.
highlighter.hooks.Serialize.RecordInfo.tap((start, end) => ({
startParagraphId: start.$node.parentElement.closest('p').dataset.id,
endParagraphId: end.$node.parentElement.closest('p').dataset.id,
}));
const sources = paragraphRanges.map(range => highlighter.fromRange(range));remove(id) and removeAll() remove the highlight wrappers and restore the original element hierarchy and attributes, including for cross-paragraph highlights. Browser text splitting and merging means the original TextNode object identities and event listeners attached directly to TextNodes are not preserved; delegate events from a stable parent element instead.
Dynamic DOM is supported after it becomes stable. For asynchronously loaded or frequently re-rendered content, initialize and restore highlights after rendering completes. The library does not support one continuous highlight across table cells, because a wrapper cannot safely span multiple td or th elements. Use the Render.SelectedNodes hook to split those selections into supported fragments.
For more details, please read this article (in Chinese).
const highlighter = new Highlighter([opts]);Creates a new Highlighter instance. opts is merged with the default options shown below.
const highlighter = new Highlighter({
$root: document,
exceptSelectors: null,
wrapTag: 'span',
style: {
className: 'highlight-mengshou-wrap',
},
});All options:
| name | type | description | required | default |
|---|---|---|---|---|
| $root | `Document | HTMLElement` | root container in which highlighting is enabled | No |
| exceptSelectors | `Array | null` | selectors for elements whose text must not be highlighted | No |
| wrapTag | string |
HTML tag used to wrap highlighted text | No | span |
| verbose | boolean |
logs library warnings and errors to the console | No | false |
| style | Object |
controls the style of highlight wrappers | No | see below |
style options:
| name | type | description | required | default |
|---|---|---|---|---|
| className | `string | string[]` | CSS class name(s) applied to highlight wrappers | No |
exceptSelectors accepts null or an array of ID, class, or tag selectors. For example, to skip h1 and .title elements:
const highlighter = new Highlighter({
exceptSelectors: ['h1', '.title'],
});Returns true when source is a valid HighlightSource; otherwise returns false.
Returns true when $node is a highlight wrapper element; otherwise returns false.
Enables automatic highlighting. When a user selects text, the library creates a highlight automatically.
Disables automatic highlighting without removing existing highlights.
Stops the highlighter, removes its event listeners, and clears internal resources. Call this when the instance is no longer needed.
Creates a highlight from a Range. Obtain one from window.getSelection().getRangeAt(0) or create one with document.createRange().
const selection = window.getSelection();
if (selection && !selection.isCollapsed && selection.rangeCount > 0) {
highlighter.fromRange(selection.getRangeAt(0));
}Highlighting splits and replaces text nodes. A native selection is live, so the browser recomputes its boundaries while that happens and may truncate it when the selection spans several text nodes. options.selection controls what happens to the native selection:
| value | behavior |
|---|---|
keep |
Default. Leaves the native selection to the caller. |
clear |
Clears the native selection before the DOM is modified. |
restore |
Selects the created wrappers after highlighting. |
// Keep the selected text selected after creating its highlight.
highlighter.fromRange(selection.getRangeAt(0), { selection: 'restore' });highlighter.run() always clears the native selection before modifying the DOM.
Restores a highlight from persisted HighlightSource data. Pass the corresponding startMeta, endMeta, text, id, and optional extra fields from a previously saved source.
Removes the highlight with the given ID. IDs are generated by web-highlighter by default; use the Render.UUID hook to provide your own IDs.
Removes every highlight under the configured root.
Adds a CSS class to the wrappers for a highlight. Omit id to apply the class to every highlight.
Removes a CSS class from highlight wrappers. Omit id to remove it from every highlight.
Returns the wrapper elements for a highlight, which may consist of several segments. Omit id to return wrapper elements for every highlight under the root.
Returns the HighlightSource for one wrapper element. It accepts the wrapper itself or one of its descendants, and returns null outside a highlight. Unlike the source emitted by the CREATE event, this source describes only the individual wrapped segment, so startMeta and endMeta can be persisted separately for a cross-node or cross-paragraph selection.
Returns the highlight ID for a DOM node. For a non-wrapper element, it looks up the nearest ancestor wrapper. Returns an empty string if none exists.
Returns the extra IDs associated with a DOM node's highlight. For a non-wrapper element, it looks up the nearest ancestor wrapper. Returns an empty array if none exists.
Updates the highlighter configuration. opt has the same shape as the constructor options and may contain only the fields you want to change.
Returns a diagnostic snapshot for bug reports. See Diagnostics for bug reports for the available evidence levels and privacy considerations.
Use .on() to subscribe to highlighter events:
const highlighter = new Highlighter();
highlighter.on(Highlighter.event.CREATE, (data, instance) => {
// ...
});Every callback receives data and the current Highlighter instance. CLICK, HOVER, and HOVER_OUT callbacks also receive the native browser event as a third argument.
Highlighter.event includes:
EventType.CLICK: a highlight is clicked.EventType.HOVER: the pointer enters a highlight.EventType.HOVER_OUT: the pointer leaves a highlight.EventType.CREATE: a highlight is created.EventType.REMOVE: a highlight is removed.
Event data:
| name | description | type |
|---|---|---|
id |
Highlight ID | string |
| name | description | type |
|---|---|---|
id |
Highlight ID | string |
| name | description | type |
|---|---|---|
id |
Highlight ID | string |
| name | description | type |
|---|---|---|
sources |
Created HighlightSource objects |
HighlightSource[] |
type |
Creation origin: from-input or from-store |
string |
The callback receives (data, instance); it does not receive a browser event. Destructure data as ({ sources, type }). Each item in sources is a HighlightSource, so pass an item—not the enclosing event data object—to Highlighter.isHighlightSource().
HighlightSource is a JSON-serializable representation of a highlight location. Store it in your backend to restore the highlight later.
| name | description | type |
|---|---|---|
ids |
Removed highlight IDs | string[] |
Hooks let you customize the highlighting lifecycle. See the advanced guide for available hooks and examples.
getDiagnostics() supports three evidence levels. Start with the default safe snapshot:
copy(JSON.stringify(highlighter.getDiagnostics(), null, 2));It includes browser/runtime capabilities, library lifecycle state, current selection shape, configuration, recent library errors, and persisted-position metadata. It intentionally does not include page HTML or selected text.
For a structure mismatch or cross-device restore issue, export a text-free DOM structure where every text node is replaced by its length:
copy(JSON.stringify(highlighter.getDiagnostics({ dom: 'redacted' }), null, 2));For a trusted/private reproduction only, you can explicitly opt in to the root HTML and original persisted sources. Review and redact this output before sharing; the DOM output is capped at 20,000 characters by default and can be adjusted with maxDomLength.
copy(JSON.stringify(highlighter.getDiagnostics({
dom: 'full',
sources: 'full',
maxDomLength: 50000,
}), null, 2));Include the suitable snapshot with a minimal HTML/JavaScript reproduction and exact interaction steps. For fromStore() failures, include the original source and the relevant DOM before and after content changes.
It depends on Selection API.
- IE 11
- Edge
- Firefox 52+
- Chrome 15+
- Safari 5.1+
- Opera 15+
Mobile support: Mobile devices are detected automatically and use touch events instead of mouse events.
web-highlighter requires browser DOM APIs, including document, Range, and Selection. It supports browsers and WebViews that provide these APIs. Native mini-program renderers and non-DOM native application views are not supported.
For more ways to customize highlighting behavior, read the advanced guide.


