突出显示文本范围

网页功能: 自定义亮点

借助 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);
  }
}