借助 CSS Custom Highlight API,您可以在不修改 DOM 结构的情况下,为页面上的任意文本范围设置样式。这样,您就可以突出显示搜索结果、使用语法着色、显示协作编辑光标或拼写和语法错误标记,而无需将文本封装在额外的元素中或依赖 innerHTML 操作。
核心实现
如需突出显示文本范围,您必须收集目标文本节点,创建 Range 和 Highlight 对象,在 HighlightRegistry 中注册它们,然后使用 ::highlight() 伪元素为其设置样式。
1. 收集文本节点并创建范围
使用 TreeWalker 收集目标元素中的所有文本节点,然后创建指向要突出显示的字符偏移量的 Range 对象。
const article = document.querySelector("article");
// MANDATORY: Use TreeWalker to collect text nodes — do not manipulate innerHTML.
const treeWalker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT);
const allTextNodes = [];
let currentNode = treeWalker.nextNode();
while (currentNode) {
allTextNodes.push(currentNode);
currentNode = treeWalker.nextNode();
}
// MANDATORY: Set range start/end on text nodes, not element nodes.
const range = new Range();
range.setStart(textNode, matchStartIndex);
range.setEnd(textNode, matchEndIndex);
缓存文本节点列表,仅在 DOM 内容实际发生更改时才重新构建该列表,因为遍历树的开销很大。
2. 根据范围创建精彩集锦
将一个或多个 Range 对象分组到一个 Highlight 中。具有相同样式的多个范围属于同一个突出显示。
const searchHighlight = new Highlight(...matchingRanges);
3. 在注册表中注册突出显示
使用 CSS.highlights(一种类似于 Map 的 HighlightRegistry)以自定义名称注册每个 Highlight。
// MANDATORY: Clear previous highlights before registering new ones
// to avoid stale ranges persisting on the page.
CSS.highlights.clear();
CSS.highlights.set("search-results", searchHighlight);
当多个突出显示区域重叠时,请使用 priority 属性控制堆叠顺序。优先级较高的突出显示会绘制在顶部。
const primary = new Highlight(...primaryRanges);
primary.priority = 1;
const secondary = new Highlight(...secondaryRanges);
secondary.priority = 0; // painted first (behind primary)
CSS.highlights.set("primary", primary);
CSS.highlights.set("secondary", secondary);
4. 使用 ::highlight() 设置样式
在 CSS 中使用 ::highlight() 伪元素,按名称为每个已注册的高亮显示设置样式。
::highlight(search-results) {
background-color: #ffdd00;
color: black;
}
只有一小部分 CSS 属性可在 ::highlight() 中使用:color、background-color、text-decoration 及其普通属性、text-shadow、-webkit-text-stroke-color、-webkit-text-fill-color 和 -webkit-text-stroke-width。background-image、font-size 或 padding 等属性会被忽略。
无障碍
AVOID:使用自定义突出显示来替代语义 HTML。
自定义亮点仅用于呈现,不会向无障碍树公开。如果突出显示的文本与文档在语义上相关(例如,用户选择的段落),请改用 <mark>。请将自定义亮点留给临时性、仅限视觉的效果,例如搜索结果或语法着色。
突出显示不应仅依赖颜色来传达含义。如果突出显示表示错误,请将其与另一个视觉指示器(例如 text-decoration: wavy underline 或相邻文本标签)搭配使用。确保突出显示的背景和文本颜色之间有足够的对比度,以满足 WCAG 2.1 的要求(对于普通文本,对比度至少为 4.5:1)。
后备策略
对于不支持 CSS Custom Highlight API 的浏览器,您应提供一种实用的基本体验,即即使没有视觉突出显示,文本仍然清晰可辨。
您可以在使用该 API 之前检测是否支持:
if (CSS.highlights) {
// CSS Custom Highlight API is supported.
} else {
// Fallback: wrap matches in <mark> elements.
}
如果突出显示对用户体验至关重要,请回退到使用 <mark> 元素封装匹配的文本。这会修改 DOM,因此请务必保留事件监听器,并避免破坏文档结构。
if (!CSS.highlights) {
// Walk text nodes and wrap matches in <mark>, preserving structure.
const walker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT);
const nodes = [];
for (let n = walker.nextNode(); n; n = walker.nextNode()) nodes.push(n);
const term = searchTerm.toLowerCase();
for (const textNode of nodes) {
const text = textNode.textContent;
let pos = text.toLowerCase().indexOf(term);
if (pos === -1) continue;
const frag = document.createDocumentFragment();
let last = 0;
while (pos !== -1) {
frag.append(text.slice(last, pos));
const mark = document.createElement("mark");
// textContent assignment avoids HTML injection.
mark.textContent = text.slice(pos, pos + term.length);
frag.append(mark);
last = pos + term.length;
pos = text.toLowerCase().indexOf(term, last);
}
frag.append(text.slice(last));
textNode.replaceWith(frag);
}
}