Enhancing Text Selection and Styling with the Custom Highlight API
The Custom Highlight API enables developers to apply CSS styles to specific text ranges defined in JavaScript without modifying the DOM. This approach avoids the performance overhead and structural complications associated with inserting <span> elements.
A typical workflow involves:
- Obtaining a text node (e.g.,
document.querySelector('p').firstChild). - Creating a
Rangeobject and defining its start and end points. - Registering this range with the API using
CSS.highlights.set()and assigning it a name. - Applying styles via the CSS
::highlight()pseudo-element with that name.
For instance, to highlight the first occurrence of a specific word within a paragraph:
const targetWord = 'example';
const highlightId = 'custom-highlight';
const textElement = document.querySelector('p').firstChild;
const fullText = textElement.textContent;
const wordStart = fullText.indexOf(targetWord);
const wordEnd = wordStart + targetWord.length;
const textRange = new Range();
textRange.setStart(textElement, wordStart);
textRange.setEnd(textElement, wordEnd);
const highlightObj = new Highlight(textRange);
CSS.highlights.set(highlightId, highlightObj);
Corresponding CSS:
::highlight(custom-highlight) {
background-color: yellow;
color: black;
}
This technique mirrors how browsers internally style text for features like the find-in-page tool, leaving the DOM structure untouched.
Key Advantages
- Performance: Eliminates the need for DOM manipulation, which can be costly when dealing with large volumes of text nodes.
- Integrity: Prevents potential side effects on other CSS or JavaScript that relies on a stable DOM structure.
- Scalability: Reduces DOM complexity, which is a known factor impacting rendering performance, animations, and scroll smoothness.
Implementing a Search Feature
A primary use case is building a custom search interface. The Highlight constructor can accept multiple Range objects, allowing a single style rule to apply to all matches.
HTML:
<label>
Search text:
<input type="search" id="searchInput" value="sample">
</label>
<p id="content">This is a sample paragraph with sample text.</p>
JavaScript:
const searchField = document.getElementById('searchInput');
const contentPara = document.getElementById('content');
const textNode = contentPara.firstChild;
searchField.addEventListener('input', (event) => {
performSearch(event.target.value);
});
function performSearch(term) {
// Clear previous highlights
CSS.highlights.clear();
if (!term) return;
const regex = new RegExp(term, 'gi');
const content = textNode.textContent;
const matches = [...content.matchAll(regex)];
const matchRanges = matches.map(match => {
const range = new Range();
range.setStart(textNode, match.index);
range.setEnd(textNode, match.index + term.length);
return range;
});
if (matchRanges.length > 0) {
const searchHighlight = new Highlight(...matchRanges);
CSS.highlights.set('search-matches', searchHighlight);
}
}
CSS for styling matches:
::highlight(search-matches) {
background-color: #ffeb3b;
text-decoration: underline wavy red;
}
Application in Syntax Highlighting
This API is well-suited for client-side syntax highlighting, as demonstrated by implementations that use parsers like Prism.js but apply styles via ranges instead of <span> elements. This can significantly reduce the number of DOM nodes.
However, a consideration is that highlighting occurs client-side, which may introduce a visible delay between rendering the raw code and applying the styles. For performance-critical applications, server-side rendering of syntax-highlighted HTML might still be preferable, provided the resulting markup remains performant and accsesible.
The API offers a powerful, low-level method for precise text styling, beneficial for features like search, annotation, and dynamic highlighting where DOM manipulation is undesirable.