サイト内検索で使うCSSカスタムハイライトAPI
X-tech推進本部 加藤当社サイトのサイト内検索ページは検索サービスとしてAlgoliaを利用しており、フロントエンドからAlgoliaの外部APIを呼び出して表示する作りになっています。
また、検索キーワードと一致するテキストが検索結果の表示エリアにある場合は、テキストをハイライトする処理を入れていて、これはCSS カスタムハイライト APIを利用しています。
カスタムハイライトAPIは、DOMを書き換えずに任意の文字範囲にスタイルを当てられるAPIです。
Algoliaのハイライト機能の制約
先にAlgoliaについて少しだけ補足ですが、Algoliaでもハイライト機能は標準機能として提供されています。
この機能を利用するとAlgoliaの検索結果に含まれるHTMLのうち、キーワードに一致するテキストが<em>で囲われた状態でHTMLが返却され、それをページに反映することでハイライトされるようになります。
しかし、この機能には以下のような制約があります。
Algolia highlights the first 50,000 characters of the whole result set (5,000 logograms for CJK languages). This limit prevents impacting the user experience by ensuring a fast response time even with large results.
Algoliaの機能でハイライトできる文字数は最大50,000文字、CJK言語だと5,000文字が上限で、それ以上はハイライトされません。
現状当社では、1ページ1レコードとして登録していて、本文をまるまる登録しているため、検索結果に含まれる文字数は割とすぐ制限を超えてしまいます。
以下は、本来ハイライトされるべきテキストが途中からハイライトされなくなっている例です。

ハイライトが途中で切れてしまっている例
ページを分割して複数レコードとして登録する方法もありますが、Algolia上の管理運用が複雑になってしまうため、Algoliaのハイライト機能は無効にし、カスタムハイライトAPIを使うことにしました。
CSSカスタムハイライトAPIとは?
改めて、CSSカスタムハイライトAPIは、DOMを書き換えずに任意の文字範囲をハイライトできるAPIです。基になるDOM構造に影響を与えずに、スタイルを適用できるのが特長です。
カスタムハイライトAPIを使ったハイライトは、いくつかの機能を組み合わせて実現します。ざっくりとした流れは以下のようになります。
- Rangeで文字範囲を作る
- HighlightにRangeをまとめる
- CSS.highlightsに登録する
- ::highlight()で見た目を指定する
サイト内検索での実装
今回の実装では、Algoliaの検索結果レンダリング後に、キーワード一致部分のRangeを作成し、CSS.highlightsへ登録しています。
const highlightHits = (hitsElement: NodeListOf<HTMLElement>) => {
const currentQuery = searchInstance.helper?.state.query;
const currentQueryLength = currentQuery?.length || 0;
if (!currentQuery || currentQueryLength < 3) {
return;
}
// 前回検索のハイライトを削除する
CSS.highlights.delete('hits');
const regExp = new RegExp(currentQuery, 'giu');
const rangeList: Range[] = [];
for (const element of hitsElement) {
const textNode = element.firstChild;
if (textNode === null || textNode.nodeType !== Node.TEXT_NODE) {
continue;
}
const matches = textNode.textContent?.matchAll(regExp);
if (!matches) {
continue;
}
for (const match of matches) {
const range = new Range();
const start = match.index;
// Rangeで文字範囲を作る
range.setStart(textNode, start);
range.setEnd(textNode, start + currentQueryLength);
rangeList.push(range);
}
}
if (rangeList.length === 0) {
return;
}
// hitsという名前で登録する
CSS.highlights.set('hits', new Highlight(...rangeList));
};
実装上のポイントは次の2点です。
renderイベントのたびにCSS.highlights.delete('hits')を実行し、前回検索のハイライトを残さない- 今回はクエリー長が2文字以下の場合は処理をスキップし、不要なRange生成を抑える
ハイライト部分に適用するスタイルは::highlight(hits)のように適用します。
.highlight::highlight(hits) {
background-color: mark;
color: marktext;
}
適用可能なプロパティは限られているためご注意ください。
また、文字の正規化方針も先に決めておくと安全です。例えば、NFKCで正規化してから比較すると、全角英数字と半角英数字、結合文字の差異を吸収しやすくなります。
要件によっては、検索語と対象文字列の両方にString.prototype.normalize('NFKC')を適用するなども考えられます。
ハイライトの意味を示すtype属性
カスタムハイライトAPIは、ハイライトの意味を示すためのtype属性を指定できます。値として指定できるハイライトタイプは以下の3つです。
spelling-error:スペルミスを示すハイライトgrammar-error:文法ミスを示すハイライトhighlight:上記のいずれにも該当しないハイライト(初期値)
ユーザーエージェントは、支援技術に対して「どういう意味でハイライトされているのか」を伝えることが期待されています。
おわりに
カスタムハイライトAPIの中核機能は、すでに主要なモダンブラウザで利用可能です。
一部のプラットフォームテストが失敗していたり、highlightsFromPointなどの新しいAPIの実装状況はまだ限定的ですが、Interop 2026の重点分野としても選ばれているので、今後は相互運用性の改善にも期待できると考えています。