v158 · css · custom highlight api · product pattern

Multi-category annotator

A working find-in-page tool on the shipped Custom Highlight API: every hit becomes a search-match highlight, the active one a higher-priority search-current highlight, and a glossary category coexists underneath. The page tries to give all three categories their shared chrome with one ::highlight(*) rule, and tells you honestly whether that rule or the per-name fallback is doing the work.

Probing this browser…

Search the article

Type to search. Glossary terms are tinted.

Text highlighting used to mean surgery: split the text node, wrap a span, restyle, and hope the editor, the selection, and the screen reader all survived the operation. The Custom Highlight API removed the surgery. A Highlight object owns a set of Range objects, the registry at CSS.highlights gives it a name, and a highlight pseudo-element paints it — the DOM never changes.

Real products layer several categories at once. A search tool paints every match, then promotes the active match with a higher priority so it wins where ranges overlap. A glossary pass tints terms it can define. A collaboration layer marks other people’s cursors. Each category is one registry entry and one rule naming it.

The repetition shows up in the stylesheet: every category that should share a base — a readable background, an underline, a focus-friendly contrast — repeats those declarations, and a new category means editing every copy. The universal selector collapses the copies into one zero-specificity base rule that every named highlight rule overrides exactly where it means to differ.

Registry state will be reported here.

the pattern

// one highlight per category — ranges collected via TreeWalker
CSS.highlights.set("search-match", matches);     // priority 0
current.priority = 2;                            // wins overlaps
CSS.highlights.set("search-current", current);
CSS.highlights.set("glossary", glossaryTerms);

/* shared chrome, once — zero specificity, every category gets it */
::highlight(*) { text-decoration: underline dotted; }

/* per-category paint stays per-category */
::highlight(search-match)   { background: … }
::highlight(search-current) { background: …; outline-style aside, it must
                              stay readable where it overlaps a match }

see also