Prompt and context
A document reader displays search matches, annotations, and the current hit in rich text. Wrapping every match in a span would disrupt selection, editing, and layout. Design a CSS Custom Highlight API solution covering Range lifecycle, registry updates, cascade, accessible states, fallback, and performance. This is a frontend question about text rendering and progressive enhancement.
What the interviewer evaluates
- Distinguish JavaScript Range, Highlight objects, and
::highlight(). - Rebuild stale ranges after text replacement or editing.
- Understand that highlights do not alter the DOM and can cascade with
::selection. - Design contrast, current-hit, focus, and keyboard semantics.
- Provide a safe fallback and quantify the cost.
Clarifying questions to ask
- Do target browsers and WebViews support the API?
- Is the document editable, virtualized, or frequently rerendered?
- How many matches exist, and can a match cross text nodes?
- Are dark mode, forced colors, and screen readers required?
- Is a marked-DOM fallback acceptable when unsupported?
A 30-second answer
“I would represent each match as a Range, group ranges in a Highlight, and register it in CSS.highlights for ::highlight(name) styling. When text changes, remove old registrations and rebuild from a text version and offsets instead of retaining detached nodes. Separate search, annotation, and current-hit layers; expose count and keyboard state beyond color. Detect support, fall back to safe marked text, and benchmark match count, update p95, frames, and memory.”
Deep-dive answer
Step 1: Create ranges and highlights
The API lets JavaScript create arbitrary text Ranges, combine them in a Highlight, and register that Highlight in the HighlightRegistry. CSS styles the ranges with ::highlight(name) without adding wrapper elements.
const range = new Range();
range.setStart(textNode, start);
range.setEnd(textNode, end);
const hit = new Highlight(range);
CSS.highlights.set("search", hit);Production code must still handle cross-node ranges and node lifetime.
Step 2: Reconcile text updates
Persist a document version, text-node path, and character offsets with each match. Rerendering or editing can detach a node, making its Range stale. Remove old registry entries and rebuild from the new text index; do not keep only DOM references.
Step 3: Manage layers
Use separate names for search, annotations, current hit, and spell-check hints. Define the intended cascade with ::selection and other user-agent highlights. Updating one current-hit object should not force every match to be rebuilt.
Step 4: Provide accessible feedback
Color cannot be the only signal. Expose current-hit count, focusable navigation, a visible focus indicator, and forced-colors alternatives. Do not assume a highlight itself is announced by a screen reader; the results list must expose semantic status.
Step 5: Control performance
Debounce input, batch registry updates, and limit work to a visible window when possible. Measure text scanning, update p95, style calculation, scroll frames, and memory. Avoid one registry mutation per keystroke.
Step 6: Progressive fallback
Feature-detect and choose Custom Highlight, marked DOM, or a plain results list. A DOM fallback must escape text, preserve links, and avoid nested duplicate wrappers. Server output should remain readable before client enhancement.
Step 7: Test and accept
Test cross-node matches, rerender, editing, dark mode, forced colors, copy, virtualization, RTL, and unsupported browsers. Compare result text, keyboard order, first paint, scroll FPS, update p95, and memory; fallback must not lose matches.
Model answer
“I would keep the document DOM unchanged: represent matches as Ranges, group them in Highlights, register names in CSS.highlights, and style them with ::highlight(). Each result carries a text version and offset; rerendering removes stale registrations and rebuilds them. Search, annotations, and current hit have separate layers, while count, focus, and semantic status make the current hit accessible.
Feature detection selects Custom Highlight or a safe marked-DOM/results-list fallback. Debounce, batch updates, and visible-window limits control cost. Acceptance covers cross-node ranges, copy, editing, dark mode, forced colors, virtualization, and unsupported browsers, comparing p95, FPS, memory, and result equality.”
Common mistakes
- Wrapping every hit in spans → disrupts text structure and selection → prefer Range/Highlight.
- Retaining old Ranges → rerenders detach nodes → rebuild by text version.
- Using color alone → forced-colors and color-blind users lose state → add semantic status and focus.
- Mutating on every key → causes style churn → debounce and batch.
- Concatenating fallback HTML → creates injection and nesting risk → escape and bound the marked range.
Follow-up questions and responses
Follow-up 1: Why not use spans?
Custom Highlight leaves the DOM unchanged, reducing interference with selection, editing, and layout. Spans remain a compatibility fallback with escaping and nesting controls.
Follow-up 2: What happens to a Range after editing?
Its node may be replaced or detached. Rebuild from the new text version and offsets instead of using the old reference.
Follow-up 3: Will screen readers announce the highlight?
Do not assume so. Expose result count, current state, and keyboard navigation as semantic text.
Follow-up 4: How do you prove no regression?
Use fixed documents and match counts to compare update p95, style work, scroll FPS, memory, copy behavior, and virtualization.