【jQuery/JS】特定の文字列をHTMLタグで囲む完全ガイド|ハイライト・mark要素・複数キーワード・XSS対策・既存タグ保持まで

検索結果でキーワードを黄色くハイライトしたり、重要語句を <strong> で囲んだりする処理はjQueryの html()replace() で実現できます。この記事では基本的な置換から大文字小文字無視・XSS対策・複数キーワード・既存HTMLタグ保持・ハイライト解除・リアルタイム検索まで実用的なパターンを解説します。

この記事でわかること

  • replace() と html() で文字列を <span>・<mark> タグで囲む基本
  • 大文字・小文字を区別しないハイライト(i フラグ)
  • ユーザー入力を使う場合の XSS・ReDoS 対策
  • 複数キーワードを一括ハイライトする
  • 既存の <strong> などタグを壊さずにハイライトする方法
  • ハイライトを解除して元のテキストに戻す
  • 入力フォームと連動したリアルタイムハイライト
スポンサーリンク

実装方法の比較

方法 既存タグ保持 手軽さ 推奨場面
text() + replace() + html() ×(タグ消える) 簡単 テキストのみの要素をハイライト
テキストノード直接操作 やや複雑 既存HTMLタグを維持したい場合
mark.js ライブラリ 簡単(CDN可) 複雑なニーズ・ナビゲーション機能も必要な場合
シンプルな用途なら text() + replace() + html() が最速
要素内が純テキストのみ(HTMLタグを含まない)なら最もシンプルな方法を使ってください。既存の <strong><a> などのタグを壊さずにハイライトしたい場合はテキストノード操作か mark.js を使います。

基本: replace() で文字列を <span> タグで囲む

text() でテキストを取得し、replace() でタグを挿入して html() で書き戻します。

<p id="target">jQueryを使ったWebサイトのサンプルテキストです。jQueryは便利です。</p>

<style>
.highlight {
  background-color: #fff176;
  font-weight: bold;
  padding: 0 2px;
  border-radius: 2px;
}
</style>
$(function () {
  var keyword = "jQuery";
  var $el = $("#target");

  // text() でHTMLタグなしのテキストを取得
  var text = $el.text();

  // replace() でキーワードを <span> で囲む
  var highlighted = text.replace(
    new RegExp("(" + keyword + ")", "g"),
    '<span class="highlight">$1</span>'
  );
  $el.html(highlighted);
});
html() ではなく text() で取得する理由
ハイライト対象のテキストを取得する際は text() を使ってください。html() で取得すると既存のHTMLタグが含まれ、置換後に <span><strong> のようなネストが発生したり、タグの属性値が誤ってハイライトされたりします。

HTML5 <mark> 要素でセマンティックにハイライト

HTML5の <mark> 要素は「検索キーワードへの該当箇所」を示すセマンティックなタグです。ブラウザのデフォルトスタイルで黄色ハイライトが適用されるため、CSSが不要です。

$(function () {
  var keyword = "jQuery";
  var $el = $("#target");

  // <mark> 要素で囲む(CSSなしでブラウザが黄色ハイライト)
  var highlighted = $el.text().replace(
    new RegExp("(" + keyword + ")", "gi"),
    "<mark>$1</mark>"
  );
  $el.html(highlighted);
});
<mark> はスクリーンリーダーにも意味が伝わる
<mark> は「参照や引用文中で関連性があるため強調された部分」を表すHTML5要素です。検索結果のキーワードハイライトや、テキスト内の参照箇所を示す用途に意味的に適切です。アクセシビリティ向上のため <span class="highlight"> より <mark> の使用を検討してください。

XSS・ReDoS 対策:ユーザー入力を正規表現に使う場合

検索フォームの入力値をそのまま new RegExp() に渡すと、.*( など正規表現の特殊文字が悪用される可能性(ReDoS)があります。ユーザー入力を使う場合は必ず特殊文字をエスケープしてください。

// 正規表現の特殊文字をエスケープする関数
function escapeRegex(str) {
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

$(function () {
  var userInput = $("#search-input").val(); // ユーザー入力
  if (!userInput.trim()) return;

  var safeKeyword = escapeRegex(userInput); // 特殊文字をエスケープ
  var $el = $("#target");

  var highlighted = $el.text().replace(
    new RegExp("(" + safeKeyword + ")", "gi"),
    "<mark>$1</mark>"
  );
  $el.html(highlighted);
});
XSS リスクと対策について
このコードは text() でHTMLエスケープされたテキストを取得してから置換します。挿入する <mark> タグは固定のコードのためXSSリスクはありません。ただし new RegExp(userInput) に未検証の入力を渡すとReDoS(正規表現を使ったDOS攻撃)のリスクがあります。escapeRegex() で必ず特殊文字をエスケープしてください。

複数キーワードを一括ハイライト

$(function () {
  var keywords = ["jQuery", "JavaScript", "DOM"];
  var $el = $("#target");

  function escapeRegex(str) {
    return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  }

  // 各キーワードをエスケープして | で連結し1つの正規表現に
  var pattern = keywords
    .map(function (kw) { return escapeRegex(kw); })
    .join("|");

  var highlighted = $el.text().replace(
    new RegExp("(" + pattern + ")", "gi"),
    "<mark>$1</mark>"
  );
  $el.html(highlighted);
});
キーワードに優先順位をつける場合は長い順に並べる
正規表現は左から順にマッチを試みます。例えば「JavaScript」と「Java」が両方キーワードの場合、["JavaScript", "Java"] の順にすると「JavaScript」が優先されます。keywords.sort((a, b) => b.length - a.length) で長い順にソートするのが安全です。

既存のHTMLタグを壊さずにハイライトする

text() を使うと要素内のすべてのタグが除去されてしまいます。既存の <strong><a> などを残したい場合は、テキストノードのみを対象に処理します。

// テキストノードのみを対象にハイライトする関数
function highlightTextNodes($el, keyword) {
  function escapeRegex(str) {
    return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  }
  var regex = new RegExp("(" + escapeRegex(keyword) + ")", "gi");

  // nodeType === 3 はテキストノード
  $el.contents().each(function () {
    if (this.nodeType === 3) {
      // テキストノードの場合: <mark> で囲んでHTMLに置換
      var newHtml = this.nodeValue.replace(regex, "<mark>$1</mark>");
      if (newHtml !== this.nodeValue) {
        $(this).replaceWith(newHtml);
      }
    } else if (this.nodeType === 1 && !$(this).is("mark, script, style")) {
      // 要素ノードの場合: 再帰的に処理(mark/script/styleは除外)
      highlightTextNodes($(this), keyword);
    }
  });
}

$(function () {
  // <strong> などの既存タグを保持したままハイライト
  highlightTextNodes($("#content"), "jQuery");
});
テキストノード操作は再帰処理が必要
ネストされた要素(例: <p><strong>テキスト</strong></p>)を処理するには子要素を再帰的に処理する必要があります。複雑なHTML構造では mark.js などのライブラリを使う方が安全です。

ハイライトを解除して元のテキストに戻す

$(function () {
  var $el = $("#target");

  // 方法1: data() で元のテキストを保存してから復元
  $el.data("original", $el.text()); // 元のテキストを保存

  // ハイライト実行
  $el.html($el.text().replace(/(jQuery)/g, "<mark>$1</mark>"));

  // 解除: 保存しておいたテキストで上書き
  $("#clear-btn").on("click", function () {
    $el.text($el.data("original"));
  });

  // 方法2: unwrap() で <mark> タグのみ除去(テキストはそのまま)
  // $(".highlight, mark").contents().unwrap();
});
unwrap() でラッパータグだけを除去できる
$("mark").contents().unwrap()<mark> タグのみを取り除き、中のテキストはそのまま残します。ただし DOM ツリーが操作された後は元の状態と微妙に異なることがあるため、完全なリセットが必要な場合は data() で元のテキストを保存して復元する方法が確実です。

リアルタイム検索ハイライト(入力連動)

入力フォームに文字が入力されるたびにハイライトを更新します。キーポイントは元のテキストを変数に保存して毎回そこから処理することです。

<input type="text" id="search-input" placeholder="キーワードを入力...">
<div id="content">
  <p>jQueryを使ったWebサイト開発は効率的です。</p>
  <p>DOMの操作やAjax通信もjQueryで簡単に実装できます。</p>
</div>
$(function () {
  // 最初に元のテキストを保存(変更前のテキストをキープ)
  var originalText = $("#content").text();

  function escapeRegex(str) {
    return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  }

  $("#search-input").on("input", function () {
    var keyword = $(this).val().trim();
    var $content = $("#content");

    if (!keyword) {
      $content.text(originalText); // 空なら元のテキストに戻す
      return;
    }

    var safeKeyword = escapeRegex(keyword);
    var highlighted = originalText.replace(
      new RegExp("(" + safeKeyword + ")", "gi"),
      "<mark>$1</mark>"
    );
    $content.html(highlighted);
  });
});
元のテキストを変数に保存することが重要
ハイライトするたびに <mark> タグが増えると、次の処理で正しく動きません。最初に元のテキストを変数に保存しておき、ハイライト処理は常に元データから行ってください。複数の要素にまたがる場合は $el.data("original", $el.text()) で各要素に紐付けて保存する方法も有効です。

ハイライトのCSSスタイル例

/* シンプルな黄色ハイライト */
.highlight, mark {
  background-color: #fff176;
  font-weight: bold;
  padding: 0 2px;
  border-radius: 2px;
}

/* マーカーペン風(下線グラデーション) */
.highlight-marker {
  background: linear-gradient(transparent 60%, #ffeb3b 60%);
  font-weight: bold;
}

/* 赤字・警告 */
.highlight-red {
  color: #e53935;
  font-weight: bold;
}

/* 現在のナビゲーション位置(アクティブキーワード) */
.highlight-current {
  background-color: #ff9800;
  color: #fff;
  border-radius: 2px;
  padding: 0 2px;
}

まとめ

jQueryで特定の文字列をHTMLタグで囲むポイントをまとめます。

  • 基本: text() で取得 → replace() でタグ挿入 → html() で書き戻し
  • セマンティック: <span class="highlight"> より <mark> を推奨(意味あり・デフォルトスタイルあり)
  • 大小無視: new RegExp(kw, "gi")i フラグ
  • ユーザー入力: escapeRegex() で正規表現特殊文字を必ずエスケープ
  • 複数キーワード: keywords.map(escapeRegex).join("|") で1つの正規表現に
  • 既存タグ保持: テキストノード(nodeType===3)を再帰処理、または mark.js
  • 解除: $("mark").contents().unwrap() または元テキストを data() で保存して復元
  • リアルタイム: 元テキストを変数に保存して毎回そこから処理

関連記事: 文字列をハイライトする完全ガイド(mark.js・ナビゲーション) / リストをリアルタイムに絞り込む完全ガイド / text()でテキストを設定・取得する完全ガイド

よくある質問(FAQ)

Q既存の <strong> などHTMLタグを保持したままハイライトしたいです。
Atext() を使うと既存のタグが取り除かれます。タグを保持するには上述のテキストノード操作(contents() + nodeType === 3)か、mark.js などの専用ライブラリを使ってください。mark.js は $(el).mark("keyword") と書くだけで既存タグを保持したまま安全にハイライトできます。
Qハイライトを解除(元に戻す)したいです。
A$("mark").contents().unwrap()<mark> タグのみを取り除き、テキストはそのまま残すことができます。または最初に元のテキストを変数(または data("original"))に保存しておき、$el.text(originalText) でリセットする方法がより確実です。
Q検索条件に特殊文字(. や * など)が入るとエラーになります。
A正規表現の特殊文字は escapeRegex() 関数でエスケープしてから new RegExp() に渡してください。str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") で全特殊文字をエスケープできます。これはReDoS(正規表現を利用したDOS攻撃)対策にもなります。
Q<span> 以外のタグ(<strong>・<em> など)でも囲めますか?
Aはい。replace() の第2引数を変えるだけで任意のタグが使えます。ただし意味を持つタグを使う場合はセマンティクスを考慮してください。<strong> は「重要性が高い」、<em> は「強調」、<mark> は「検索キーワードへの該当」という意味を持ちます。見た目だけの変更なら <span class="..."> が適切です。
Q複数の要素に対して一括ハイライトしたいです。
A$(".target-class").each() でループすることで複数要素に適用できます:
$(".target").each(function () { var $el = $(this); $el.html($el.text().replace(/(keyword)/gi, "<mark>$1</mark>")); });
元テキストを保存しておく場合は $el.data("original", $el.text()) を each の最初に実行してください。