原文链接:Using the Custom Highlight API,2025 年 8 月 7 日,by Chris Coyier。
最近,自定义高亮 API(Custom Highlight API)引起了我的注意------火狐浏览器(Firefox 140 版本,2025 年 6 月)刚刚开始支持该 API,至此所有主流浏览器均已兼容。
借助这个 API,我们可以通过 JavaScript 中的 Range()
类,对获取到的文本应用(部分)样式。虽然我更愿意说它是"对选中的文本"进行操作,因为对于习惯使用 CSS 的人来说,高亮 API 的操作方式比较特别,它并不涉及常规的选择器,
我觉得有必要先简单解释一下核心概念------要是我刚开始研究这个 API 时有人这么告诉我就好了:
- 首先需要一个
textNode
(文本节点)。(例如document.querySelector("p").firstChild
) - 接着需要创建一个
Range()
(范围对象),并调用其setStart
和setEnd
方法,通过两个整数参数定义文本范围的起始和结束位置。 - 然后调用
CSS.highlights.set()
方法,将这个 Range 对象与一个名称关联起来。 - 最后在 CSS 中使用
::highlight()
伪元素,并传入刚才定义的名称,即可为目标文本应用样式。
假设页面上有一个包含文本的 <p>
标签,那么高亮的完整实现流程如下:
javascript
const WORD_TO_HIGHLIGHT = "wisdom"; // 要高亮的单词
const NAME_OF_HIGHLIGHT = "our-highlight"; // 高亮样式的名称
// 1. 获取文本节点(<p> 标签的第一个子节点,即文本内容)
const textNode = document.querySelector("p").firstChild;
// 获取文本节点的内容
const textContent = textNode.textContent;
// 2. 计算要高亮文本的起始和结束索引
const startIndex = textContent.indexOf(WORD_TO_HIGHLIGHT);
const endIndex = startIndex + WORD_TO_HIGHLIGHT.length;
// 3. 创建 Range 对象并定义范围
const range = new Range();
range.setStart(textNode, startIndex); // 从起始索引开始
range.setEnd(textNode, endIndex); // 到结束索引结束
// 4. 创建 Highlight 对象并关联到 CSS 高亮范围
const highlight = new Highlight(range);
CSS.highlights.set(NAME_OF_HIGHLIGHT, highlight);
css
::highlight(our-highlight) {
color: red;
text-shadow: 0 0 5px white, 0 0 10px red;
}
查看效果,可以看到"wisdom" 这个单词被应用了自定义 CSS 样式,但通过开发者工具会发现它周围并没有我们通常认为"必须存在"的包裹元素(比如 <span>
)。

这种方式类似浏览器自身实现的查找逻辑------比如当你使用浏览器内置的"查找"功能时,浏览器就是这样为部分文本应用样式的。


为什么这个 API 有用?
- 无需修改 DOM 就能定位文本并为其应用样式,这种能力本身就很有价值。DOM API 常被诟病性能不佳,因此避免操作 DOM 可能会带来优势,尤其是在需要频繁处理文本样式的场景中。
- 要知道,添加或删除
<span>
标签不仅可能"变慢",还会改变 DOM 结构,进而影响其他依赖 DOM 的 CSS 样式和 JavaScript 逻辑。 - DOM 重量(DOM weight)是网页性能的重要影响因素。DOM 元素过多会导致重排重绘的"成本"剧增,页面体验会随之下降,比如动画卡顿、滚动不流畅等。
举个例子:某 GitHub 的 PR(拉取请求)页面仅修改了 17 个文件,但页面中已经有超过 4500 个 <span>
标签------它们用于代码差异的颜色标注和语法高亮。这个数量已经相当可观了,而且实际场景中可能会更糟。

我相信这个 API 存在的原因还有很多,但以上这些是我立刻能想到的几个核心优势。
进阶用法(搜索功能示例)
new Highlight()
构造函数支持传入多个 Range 对象。这意味着,一个 CSS 中的 ::highlight()
伪元素可以作用于多个文本范围。这个特性非常适合实现页面自带的搜索功能------如果搜索是你正在开发的 Web 应用的核心功能,那么构建自定义搜索界面,无疑比依赖浏览器内置的查找功能更灵活。
下面我们实现一个"用户输入关键词,页面高亮匹配文本"的功能:
首先,创建一个搜索输入框:
html
<label>
搜索下方文本
<input type="search" value="oven" id="searchTerm"> <!-- 默认值为 "oven" -->
</label>
然后,监听输入框的内容变化事件:
javascript
// 监听输入框的文本变化(实时搜索)
window.searchTerm.addEventListener("input", (e) => {
// 将输入的文本转为小写后传入搜索函数(搜索通常需要忽略大小写)
doSearch(e.target.value.toLowerCase());
});
注意:我们将输入的文本转为小写后再传入搜索函数,因为忽略大小写的搜索对用户来说通常更实用。
接下来,doSearch
函数会接收搜索关键词,并通过正则表达式在所有文本中匹配:
javascript
// 在 doSearch 函数中创建正则表达式(g:全局匹配,i:忽略大小写)
const regex = new RegExp(searchTerm, "gi");
我们需要获取所有匹配文本的起始索引,并将其存入数组。实现代码虽然有点长,但逻辑很清晰:
javascript
// 获取所有匹配文本的起始索引(theTextContent 是页面中要搜索的完整文本)
const indexes = [...theTextContent.matchAll(new RegExp(searchTerm, 'gi'))].map(a => a.index);
有了索引数组后,我们就可以循环创建 Range 对象,再将所有 Range 传入 Highlight 构造函数:
javascript
const arrayOfRanges = []; // 存储所有 Range 对象的数组
indexes.forEach(matchIndex => {
// 为每个匹配的文本创建 Range 对象
const searchRange = new Range();
// par 是要搜索的文本节点(与前文的 textNode 类似)
searchRange.setStart(par, matchIndex);
searchRange.setEnd(par, matchIndex + searchTerm.length); // 结束位置 = 起始索引 + 关键词长度
arrayOfRanges.push(searchRange); // 将 Range 对象加入数组
})
// 创建 Highlight 对象(传入所有 Range),并关联到 CSS 高亮集合
const ourHighlight = new Highlight(...arrayOfRanges);
CSS.highlights.set("search-results", ourHighlight); // 高亮名称为 "search-results"
把这些代码整合起来,就能实现一个功能完整的实时搜索高亮效果了。
javascript
const NAME_OF_HIGHLIGHT = "search-results";
// 监听输入框的文本变化(实时搜索)
window.searchTerm.addEventListener("input", (e) => {
// 将输入的文本转为小写后传入搜索函数(搜索通常需要忽略大小写)
doSearch(e.target.value.toLowerCase());
});
function doSearch(searchTerm) {
// 创建正则表达式(g:全局匹配,i:忽略大小写)
const regex = new RegExp(searchTerm, "gi");
// 获取所有匹配文本的起始索引(theTextContent 是页面中要搜索的完整文本)
const indexes = [...theTextContent.matchAll(new RegExp(searchTerm, 'gi'))].map(a => a.index);
const arrayOfRanges = []; // 存储所有 Range 对象的数组
indexes.forEach(matchIndex => {
// 为每个匹配的文本创建 Range 对象
const searchRange = new Range();
// par 是要搜索的文本节点(与前文的 textNode 类似)
searchRange.setStart(par, matchIndex);
searchRange.setEnd(par, matchIndex + searchTerm.length); // 结束位置 = 起始索引 + 关键词长度
arrayOfRanges.push(searchRange); // 将 Range 对象加入数组
})
// 创建 Highlight 对象(传入所有 Range),并关联到 CSS 高亮集合
const ourHighlight = new Highlight(...arrayOfRanges);
CSS.highlights.set("search-results", ourHighlight); // 高亮名称为 "search-results"
}
// 获取文本节点(<p> 标签的第一个子节点,即文本内容)
const par = document.querySelector("p").firstChild;
// 获取文本节点的内容
const theTextContent = textNode.textContent.toLowerCase();
doSearch("oven");
css
::highlight(search-results) {
color: orange;
}
html
<main>
<label>
Search the text below
<input type="search" value="oven" id="searchTerm">
</label>
<p>The pizza oven was very hot and it burned the pizza. But the pizza was delicious anyway, I don't blame the oven.</p>
</main>
<script src="./script.js"></script>

用于语法高亮
自定义高亮 API 非常适合实现代码的语法高亮功能。André Ruffert 已经实践了这个想法:他创建了一个 Web 组件,先通过 Lea Verou 开发的 Prism.js 对代码进行分词(tokenize),但没有像默认的 Prism 那样用 <span>
标签包裹不同语法部分,而是改用这个自定义高亮 API 来应用样式。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My New Pen</title>
<link rel="stylesheet" href="./style.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/syntax-highlight-element@1/dist/themes/prettylights.min.css">
</head>
<body>
<main>
<syntax-highlight language="js">let str = "";
for (let i = 0; i < 9; i++) {
str += i;
}
console.log(str);
// Expected output: "012345678"
</syntax-highlight>
</main>
<script type="module" src="./script.js"></script>
</body>
</html>
javascript
import 'syntax-highlight-element';
demo:

我觉得这个思路非常棒,但需要注意的是:该 API 只能在客户端(浏览器)使用。对于语法高亮来说,这意味着用户会看到"先显示原始代码,再加载语法高亮"的延迟。
说实话,我更倾向于在可能的情况下使用服务端渲染的语法高亮------如果能从服务端直接返回带有 <span>
标签(用于语法高亮)的代码,并且这种方式不会严重影响性能和可访问性,那么服务端方案可能更优。
另外,我还对"自带语法高亮的字体"很感兴趣------这种字体似乎是字体厂商尚未开发的领域,潜力巨大。