Fast In-Page Search With CSS Custom Highlight API

This article explains the CSS Custom Highlight API, a modern web platform feature designed to style arbitrary text ranges without altering the underlying DOM structure. It explores the performance bottlenecks associated with traditional DOM-based text highlighting techniques, details the technical mechanics of the Custom Highlight API, and illustrates why this native approach drastically accelerates in-page text search operations in large or dynamic documents.

The Problem with Traditional Text Highlighting

Implementing in-page search historically required modifying the Document Object Model (DOM). When a search query matches text on a page, scripts typically wrap each match in an inline element, such as a <mark> or <span> tag.

While this technique works for small documents, it introduces severe performance bottlenecks:

  • DOM Fragmentation: Wrapping text breaks single Text nodes into multiple child nodes. Normalizing and restoring these nodes when clearing or updating search results is computationally expensive.
  • Layout Thrashing and Reflows: Inserting and removing elements triggers forced synchronous layouts, style recalculations, and repaints. As a user types into a search input, recurring full-page reflows result in noticeable input lag and dropped frames.
  • Complex State Management: Interleaved markup can interfere with accessibility tools, event listeners, and dynamic UI frameworks that track DOM node references.

What is the CSS Custom Highlight API?

The CSS Custom Highlight API provides a native mechanism to style text ranges programmatically without touching the DOM tree. It bridges JavaScript's ability to locate text with the browser's native rendering engine via the ::highlight() pseudo-element.

Instead of inserting HTML elements, developers create standard JavaScript Range objects pointing to start and end offsets within existing text nodes. These ranges are grouped into a Highlight object, registered globally via the CSS.highlights registry, and styled using pure CSS.

How the API Speeds Up Search Highlighting

The Custom Highlight API speeds up search highlighting by decoupling the visual representation of text from the document structure.

1. Zero DOM Mutations

Because the API operates strictly through Range objects, no new nodes are injected into the document. The DOM tree remains entirely untouched, completely eliminating DOM mutation overhead and the risk of breaking surrounding application logic.

2. Elimination of Layout Reflows

Styling a pseudo-element bypasses the browser's layout (reflow) stage. The browser does not need to recalculate geometry, margins, or element boundaries. Painting the highlight occurs directly in the rendering pipeline alongside standard text drawing, drastically reducing frame processing times.

3. Immediate Live-Search Updates

When search queries change in real time, the application only needs to update the set of ranges within the Highlight object. The browser repaints only the affected text runs. This allows applications to highlight thousands of search matches across large documents—such as code repositories, books, or data tables—at a consistent 60 frames per second.

Implementation Workflow

Using the API involves four concise steps:

  1. Find Matches: Traverse text nodes using standard DOM APIs (such as TreeWalker) and create Range objects for every match.

  2. Create a Highlight: Pass the matching ranges into a new Highlight instance:

    const searchHighlight = new Highlight(...matchingRanges);
  3. Register the Highlight: Add the instance to the global highlight registry with an identifier:

    CSS.highlights.set("search-results", searchHighlight);
  4. Apply CSS: Style the registered highlight using the ::highlight() pseudo-element:

    ::highlight(search-results) {
      background-color: #ffeb3b;
      color: #000000;
    }

To clear or update the highlights, you update the registered Highlight object or call CSS.highlights.delete("search-results"). The browser instantly clears the visual styles without requiring DOM cleanup.