Selectors Advanced

Text highlighting without DOM manipulation

Highlighting arbitrary text used to mean mutating the DOM: wrapping ranges in <mark> elements via innerHTML.replace() or surroundContents(). The CSS Custom Highlight API lets you style ranges directly in CSS with no DOM changes.

βœ“ Modern
9 lines
1/* CSS */
2::highlight(search) { background: yellow; }
3
4/* JS β€” no DOM changes */
5const range = new Range();
6range.setStart(node, startOffset);
7range.setEnd(node, endOffset);
8CSS.highlights.set(
9  'search', new Highlight(range));
βœ• Old 9 lines
1/* Mutates the DOM β€” loses event listeners */
2function highlight(el, term) {
3  el.innerHTML = el.innerHTML
4    .replace(
5      new RegExp(term, 'gi'),
6      '<mark>$&</mark>'
7    );
8}
9/* Breaks across element boundaries */
Newly available Since 2025 93% global usage

Since 2025 this feature works across the latest devices and browser versions. This feature might not work in older devices or browsers.

105+
140+
17.2+
105+
Interop 2026 focus area ? Learn more β†’
type to highlight β€” no DOM changes
Modern CSS has brought features like container queries, cascade layers, and the CSS Custom Highlight API to replace heavy JavaScript solutions with elegant, declarative patterns.
::highlight(search) + CSS.highlights.set()
⚑

No DOM mutations

The DOM stays untouched. No wrapped <mark> elements to insert or remove, no lost event listeners, no broken element boundaries.

✦

Crosses element boundaries

Range can span across multiple elements. The old surroundContents() approach throws when a range crosses tag boundaries.

∞

Multiple highlight layers

Register as many named highlights as you need. Each gets its own ::highlight(name) pseudo-element and its own styles.

Old Approach
innerHTML.replace()
Mutates DOM, breaks boundaries
Modern Approach
CSS.highlights.set()
Zero DOM changes
Lines Saved
9 β†’ 8
Plus no cleanup needed

How it works

The classic approach was innerHTML.replace(): grab the element's HTML as a string, wrap matches in <mark>, and write it back. This nukes event listeners, breaks React and other framework rendering, and silently fails when a match crosses a tag boundary.

The CSS Custom Highlight API keeps highlighting entirely separate from the DOM. You create a Range, pass it to new Highlight(range), register it with CSS.highlights.set('name', highlight), and style it with the ::highlight(name) pseudo-element in CSS. No elements are created or destroyed.

To clear highlights, call CSS.highlights.delete('name') or CSS.highlights.clear(). Firefox 140 shipped support in June 2025, completing full cross-browser coverage. This is an Interop 2026 focus area.

New CSS drops.

Join 500+ readers who've survived clearfix hacks.

ESC