CSS Custom Highlight API and ::highlight()
The CSS Custom Highlight API provides a performant mechanism for
styling arbitrary text ranges directly from JavaScript without modifying
the underlying DOM structure. By decoupling text selection logic from
HTML markup, developers can register ranges into a global registry and
style them declaratively using the ::highlight(custom-name)
pseudo-element. This article explains how the API works, how to register
highlights in JavaScript, and how to style them using CSS.
The Problem with Traditional Text Styling
Traditionally, dynamically styling text—such as search result
highlights, spell-checking markers, or collaborative editing
cursors—required wrapping target text inside inline HTML elements like
<mark> or <span>. This approach
introduces significant drawbacks:
- DOM Thrashing: Creating, inserting, or removing wrapper tags forces style recalculations and layout reflows, causing performance bottlenecks in large documents.
- State Issues: Modifying the DOM can reset selection ranges, interfere with text mutation observers, and disrupt accessibility trees.
The CSS Custom Highlight API solves these issues by styling text ranges in an overlay layer without altering the DOM hierarchy.
How the API Works
Using the API involves a four-step process spanning JavaScript and CSS:
- Create Ranges: Create one or more standard
JavaScript
Rangeobjects identifying the text offsets to style. - Instantiate a Highlight: Pass the range objects to
a
new Highlight()constructor. - Register the Highlight: Add the
Highlightinstance to theCSS.highlightsregistry with a custom identifier. - Apply CSS: Style the registered name using the
::highlight(custom-name)pseudo-element.
Step-by-Step Implementation
1. Creating and Registering Ranges in JavaScript
First, identify the text nodes and create standard DOM
Range instances:
// Target a text node
const textNode = document.querySelector("#content").firstChild;
// Define the start and end offsets
const range1 = new Range();
range1.setStart(textNode, 0);
range1.setEnd(textNode, 5);
const range2 = new Range();
range2.setStart(textNode, 10);
range2.setEnd(textNode, 18);
// Group the ranges into a Highlight object
const searchResults = new Highlight(range1, range2);
// Register the highlight under a custom name
CSS.highlights.set("search-results", searchResults);2. Styling the Highlight in CSS
Once registered, the highlight name can be targeted globally or
scoped to specific elements using the ::highlight()
pseudo-element:
::highlight(search-results) {
background-color: #fde047;
color: #000000;
text-decoration: underline;
}You can also scope highlights to specific containers:
article.search-area::highlight(search-results) {
background-color: #38bdf8;
color: #ffffff;
}Supported CSS Properties
Because highlight pseudo-elements do not create standard layout
boxes, they only accept a restricted subset of CSS properties—similar to
::selection:
colorbackground-colortext-decorationand its longhands (text-decoration-line,text-decoration-color, etc.)text-shadowstroke-color,fill-color, andstroke-width(for SVG text)
Properties that alter layout (such as margin,
padding, display, or font-size)
are ignored.
Handling Overlapping Highlights and Priorities
When multiple custom highlights overlap on the same text, conflicts
are resolved using the priority property. Highlights with a
higher priority number render on top of those with lower numbers:
const userNote = new Highlight(range1);
userNote.priority = 1;
const syntaxError = new Highlight(range1);
syntaxError.priority = 2; // Renders on top of userNote
CSS.highlights.set("user-note", userNote);
CSS.highlights.set("syntax-error", syntaxError);If priorities are equal, the order in which the highlights were
registered in CSS.highlights determines rendering order,
with the most recently added highlight taking precedence. Default
browser selections (::selection) always render on top of
custom highlights.