A Guide to the CSS Custom Highlight API

developer in a coffee shop working on developing a code editor with code symbols in the air

The CSS Custom Highlight API is a powerful tool that allows developers to style arbitrary text ranges on a webpage using JavaScript and CSS. It extends beyond traditional highlight pseudo-elements like `::selection`, `::spelling-error`, and `::grammar-error`, enabling the creation of custom highlights for text ranges without altering the DOM.

In this blog post, we’ll explore the CSS Custom Highlight API, its key concepts, and how you can use it to highlight text programmatically. By the end, you’ll have a fully functional example that highlights search results on a webpage.

Why Use the CSS Custom Highlight API?

Styling text ranges programmatically is a common requirement for applications like:

  • Text editors: Highlight spelling or grammar errors.
  • Code editors: Highlight syntax errors or specific keywords.
  • Search tools: Highlight search terms in documents. 

The CSS Custom Highlight API makes this process seamless by allowing developers to:

  • Create arbitrary `Range` objects in JavaScript.
  • Define highlights using the `Highlight` interface.
  • Register highlights in the `HighlightRegistry`.
  • Style highlights using the `::highlight()` pseudo-element.

Key Concepts

To use the CSS Custom Highlight API, follow these four steps:

  1. Create `Range` objects: Define the text ranges to style.
  2. Create `Highlight` objects: Associate the ranges with highlights.
  3. Register highlights: Use the `HighlightRegistry` (`CSS.highlights`) to register highlights.
  4. Style highlights: Use the `::highlight()` pseudo-element to define CSS styles.

HTML

				
					<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSS Custom Highlight API Demo</title>
    <link rel="stylesheet" href="style.css">
    <script src="customhighlight.js" defer></script>
</head>

<body>
    <h1>CSS Custom Highlight API Demo</h1>
    <label>Search within text <input id="query" type="text" /></label>
    <article>
        <p>
            Maxime debitis hic, delectus perspiciatis laborum molestiae labore,
            deleniti, quam consequatur iure veniam alias voluptas nisi quo. Dolorem
            eaque alias, quo vel quas repudiandae architecto deserunt quidem, sapiente
            laudantium nulla.
        </p>
        <p>
            Maiores odit molestias, necessitatibus doloremque dolor illum reprehenderit
            provident nostrum laboriosam iste, tempore perferendis! Ab porro neque esse
            voluptas libero necessitatibus fugiat, ex, minus atque deserunt veniam
            molestiae tempora? Vitae.
        </p>
        <p>
            Dolorum facilis voluptate eaque eius similique ducimus dignissimos assumenda
            quos architecto. Doloremque deleniti non exercitationem rerum quam alias
            harum, nisi obcaecati corporis temporibus vero sapiente voluptatum est
            quibusdam id ipsa.
        </p>
    </article>
</body>

</html>
				
			

CSS

				
					/* Define a custom highlight style */
::highlight(search-results) {
    background-color: #f06;
    color: white;
}
				
			

JS

				
					const query = document.getElementById('query');
const article = document.querySelector('article');

// Find all text nodes in the article. We'll search within
// these text nodes.
const treeWalker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT);
const allTextNodes = [];
let currentNode = treeWalker.nextNode();
while (currentNode) {
	allTextNodes.push(currentNode);
	currentNode = treeWalker.nextNode();
}

// Listen to the input event to run the search.
query.addEventListener('input', () => {
	// If the CSS Custom Highlight API is not supported,
	// display a message and bail-out.
	if (!CSS.highlights) {
		article.textContent = 'CSS Custom Highlight API not supported.';
		return;
	}

	// Clear the HighlightRegistry to remove the
	// previous search results.
	CSS.highlights.clear();

	// Clean-up the search query and bail-out if
	// if it's empty.
	const str = query.value.trim().toLowerCase();
	if (!str) {
		return;
	}

	// Iterate over all text nodes and find matches.
	const ranges = allTextNodes
		.map((el) => {
			return { el, text: el.textContent.toLowerCase() };
		})
		.map(({ text, el }) => {
			const indices = [];
			let startPos = 0;
			while (startPos < text.length) {
				const index = text.indexOf(str, startPos);
				if (index === -1) break;
				indices.push(index);
				startPos = index + str.length;
			}

			// Create a range object for each instance of
			// str we found in the text node.
			return indices.map((index) => {
				const range = new Range();
				range.setStart(el, index);
				range.setEnd(el, index + str.length);
				return range;
			});
		});
	// Create a Highlight object for the ranges.
	const searchResultsHighlight = new Highlight(...ranges.flat());

	// Register the Highlight object in the registry.
	CSS.highlights.set('search-results', searchResultsHighlight);
});

				
			
more posts:
Online video streaming platform and digital content management concept with media player interface, cloud library, and multimedia files on laptop, representing content distribution, video marketing
Code

Breaking into the Pipeline: Real-Time Frame Manipulation with the Insertable Streams for MediaStreamTrack API

The experimental Insertable Streams for MediaStreamTrack API closes a long-standing gap in web multimedia by treating live camera feeds as standard Web Streams. By piping individual VideoFrame objects through a TransformStream and recording the transformed output via MediaRecorder, developers can execute frame-by-frame processing—like real-time filtering, watermarking, and computer vision overlays—without relying on hidden canvas hacks.

Discover More »
Apps & Custom Software

What Building Our Own App Taught Us About Building Yours

When Hess WebTech built and launched Tericipe—an Android app that turns printed recipes into a searchable digital collection—in under three months, we had to live with every decision we usually advise clients on. From testing against real-world messy data and handling AI uncertainty to server-side payment security and post-launch maintenance, here are five crucial lessons from our own build that apply to almost any software project.

Discover More »