CSS Custom Highlight API

无需修改 DOM,用 Range + CSS 实现文本高亮

什么是 CSS Custom Highlight API?

CSS Custom Highlight API 是一项现代 Web 技术,允许开发者以编程方式创建任意文本范围的高亮效果,而无需修改 DOM 结构。传统的文本高亮方案通常需要将目标文本包裹在 <mark> 或 <span> 等元素中,这不仅会改变 DOM 树,还可能破坏原有的样式和布局。

该 API 的核心由三个部分组成:Range 对象用于描述文本范围,Highlight 对象用于将多个 Range 组合成一组高亮,CSS.highlights 注册表用于管理所有命名高亮。通过 CSS 中的 ::highlight() 伪元素选择器,开发者可以为每组高亮定义独立的视觉样式。

核心工作原理

使用 CSS Custom Highlight API 的基本步骤如下:首先,使用 TreeWalker 或其他方式遍历 DOM 中的文本节点,找到所有匹配的文本位置;然后为每个匹配位置创建一个 Range 对象,设置其起始和结束偏移量;接着将这些 Range 传入 new Highlight() 构造函数,创建一个 Highlight 实例;最后调用 CSS.highlights.set('my-highlight', highlight) 注册该高亮,并在 CSS 中编写 ::highlight(my-highlight) 规则来定义样式。

值得注意的是,Highlight API 对性能非常友好。由于高亮效果完全在渲染层处理,不触及 DOM 树,因此即使在包含大量文本的页面中频繁更新高亮,也不会触发不必要的重排(reflow)或重绘(repaint)。这使其非常适合实现实时搜索高亮、代码编辑器语法高亮等场景。

与传统方案的对比

在 CSS Custom Highlight API 出现之前,开发者若要高亮文本,通常会使用 innerHTML 替换将匹配文本包裹在 <mark> 标签中,或者使用 execCommand('hiliteColor') 等已废弃的命令。前者存在 XSS 安全风险且会破坏 DOM 结构,后者则已不被推荐使用。Selection API 配合 Range 可以实现高亮,但仅限于用户当前的选择状态,无法同时维持多个独立的高亮组。

Custom Highlight API 彻底解决了这些问题:它是纯声明式的,通过 CSS 控制样式;它支持多组命名高亮同时存在,互不干扰;它不修改 DOM,因此对页面结构零侵入;它与浏览器的文本渲染管线深度集成,性能出色。目前,Chrome 105+、Edge 105+、Safari 17.2+ 和 Firefox 131+ 均已支持该 API。

实际应用场景

CSS Custom Highlight API 的应用场景非常广泛。在搜索功能中,可以实时高亮用户输入的关键词,并用不同颜色区分当前焦点匹配和其他匹配;在代码编辑器中,可以实现语法高亮、错误标注、选中引用高亮等功能,Monaco Editor 等知名编辑器已经开始采用该 API;在文档阅读器中,可以支持用户自定义笔记高亮,并将高亮数据序列化存储,下次访问时恢复。

此外,该 API 还可以用于实现拼写检查下划线、翻译工具的词语标注、无障碍功能中的焦点指示,以及各种富文本编辑器的选区反馈。随着浏览器支持率的不断提升,CSS Custom Highlight API 正在成为前端文本处理领域的重要基础设施。

实现步骤

1

遍历文本节点

使用 TreeWalker 遍历容器内所有 Text 节点,收集纯文本内容供后续匹配。

2

创建 Range

对每个文本节点执行 indexOf 查找,为每处匹配调用 document.createRange() 并设置起止偏移量。

3

注册 Highlight

将所有 Range 传入 new Highlight(),再调用 CSS.highlights.set(name, highlight) 完成注册。

4

CSS 定义样式

在样式表中用 ::highlight(name) 伪元素为高亮组设置背景色、文字色等视觉样式,无需接触 DOM。

核心代码

highlight.jsJavaScript
// Step 1 — 遍历目标容器内所有文本节点
const walker = document.createTreeWalker(
  root, NodeFilter.SHOW_TEXT
);
const nodes = [];
while ((node = walker.nextNode())) nodes.push(node);

// Step 2 — 在每个文本节点中查找匹配,创建 Range
const ranges = [];
for (const node of nodes) {
  let startPos = 0;
  while (startPos < node.textContent.length) {
    const idx = node.textContent.toLowerCase()
      .indexOf(query.toLowerCase(), startPos);
    if (idx === -1) break;
    const range = document.createRange();
    range.setStart(node, idx);
    range.setEnd(node, idx + query.length);
    ranges.push(range);
    startPos = idx + 1;
  }
}

// Step 3 — 用 Highlight 包装 Range,注册到 CSS.highlights
CSS.highlights.set("search-result", new Highlight(...ranges));

// Step 4 — 在 CSS 中用 ::highlight() 伪元素定义样式(无需改 DOM)
::highlight(search-result) {
  background-color: oklch(0.85 0.22 95 / 85%);
  color: oklch(0.13 0 0);
}
Built with v0