mailwal

Highlight search matches without touching the DOM

Marko Denic

Highlight search matches without touching the DOM

Read on my website / Read time: 4 minutes

Brought to you by:

Know someone who'd use this? Forward it.

You add a "filter this page" box to your docs, your FAQ or a long settings screen. The user types "refund", and now you want every match to light up. The classic fix is to find the text and wrap each match in a <mark>. Then you unwrap them all on the next keystroke, and hope you didn't break an event listener, a framework's render, or a word split across two text nodes.

The browser can paint highlights for you now. The CSS Custom Highlight API takes a list of text ranges and styles them, without adding a single element to your page.

The fix

The tree walker visits every text node inside the article. For each match, a Range marks where it starts and ends. All the ranges go into one Highlight, registered under the name search, and ::highlight(search) styles them. Your HTML stays exactly as it was.

Try the live demo: type "the" or "refund" and watch the matches light up. Press Enter to jump to the next one.

The pieces

  • Range: a start and an end position inside the page's text. It doesn't change anything by itself, it just points at the text.
  • new Highlight(...ranges): a group of ranges that get the same style.
  • CSS.highlights.set('search', ...): registers the group under a name. Setting it again replaces the old matches, and an empty Highlight clears them. No cleanup code.
  • ::highlight(search): styles that group. It accepts color, background-color, text-decoration and text-shadow. No padding, borders or layout, because nothing on the page is allowed to move.

Jump to the next match

A second highlight marks the current match, and Enter moves it:

priority = 1 paints the current match on top of the yellow ones. The second input listener resets it, so a new query starts from the first match.

Where it works

  • Every current browser: Chrome, Edge, Safari and Firefox all support it. Check if (CSS.highlights) before using it, and older browsers simply show no highlights.
  • Screen readers don't see it: a highlight is paint, not content. Announce the result count in an aria-live region, like "4 matches", so everyone knows what happened.
  • One text node at a time: a match split across elements, like re<b>fund</b>, won't be found. For search boxes, that is almost never a problem.
  • Big pages: walking thousands of text nodes on every keystroke adds up. Debounce the input and it stays smooth.

Where this comes up

  • Docs and FAQs: a "filter this page" box that shows where the answer is.
  • Long settings screens: type "email" and see every related option.
  • Tables and logs: spot every occurrence of an ID or an error code.
  • Command palettes: highlight the matching part of each result in your Cmd+K menu.

Why this matters

  • Your DOM stays untouched: no wrapping, no unwrapping, no broken event listeners or framework renders.
  • It's fast: the browser only repaints. Nothing is re-laid out, because nothing on the page moves.
  • Clearing is one line: set an empty highlight, and every match is gone.

Find the longest page in your app, add a search box on top, and paste in the fix. Your users will find answers before they think of contacting support.

Thanks for reading!

See you next Saturday.

Marko Denić

Find me online:
X/Twitter / LinkedIn / GitHub
Tools I use