Highlight search matches without touching the DOM
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 emptyHighlightclears them. No cleanup code.::highlight(search): styles that group. It acceptscolor,background-color,text-decorationandtext-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-liveregion, 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.