テキスト範囲をハイライト表示する

ウェブの機能: カスタム ハイライト

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. 範囲からハイライトを作成する

1 つ以上の Range オブジェクトを Highlight にグループ化します。同じスタイルを共有する複数の範囲が 1 つのハイライトに属します。

const searchHighlight = new Highlight(...matchingRanges);

3. レジストリにハイライトを登録する

Map のような HighlightRegistry である CSS.highlights を使用して、各 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;
}

::highlight() 内で機能する CSS プロパティは、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);
  }
}