Live demo: search-highlighting-demo.html (BEFORE/AFTER)
Common failure modes
- Color-only highlight: “matches” are just blue text (hard to see; fails non-color redundancy).
- No count: the UI never says how many matches exist or where you are in the sequence.
- Unsafe rendering: user input is injected into
innerHTML(XSS risk, brittle markup). - Focus theft: “Next match” moves keyboard focus to a highlight unexpectedly.
- Motion surprises: auto-scrolling animates even when the user prefers reduced motion.
Minimum viable pattern
- Use
<mark>(or an equivalent semantic element) to wrap matches. - Make the highlight a shape: background + underline/outline (not just a color change).
- Show a calm count via a polite live region: “7 matches for “foo””.
- Support next/prev match navigation. Prefer scrolling the current match into view without forcing focus.
- Rebuild highlights safely from
textContent/ text nodes; don’t inject raw term strings into HTML.
One small detail that matters: a “current match” marker
When there are many matches, users need a distinct marker for the current one (e.g., thicker outline + different background). Pair it with status text like “Current: 3 of 12”.
Pseudo-code sketch
// Given a list of paragraphs (strings), rebuild DOM safely.
// 1) escape user term for regex
// 2) split text into [before, match, after] pieces
// 3) append text nodes + <mark> nodes
// 4) announce counts with role=status (polite)
function render(term){
container.textContent = '';
marks = [];
for (p of paragraphs){
el = document.createElement('p');
appendWithMarks(el, p, regex(term));
container.appendChild(el);
}
status.textContent = `${marks.length} matches for “${term}”`;
}
Tip: handle empty matches defensively (avoid regex loops on zero-length matches).