【jQuery/JS】検索キーワードをハイライトする完全ガイド|mark.js・前後ナビ・URL自動ハイライト・複数色・debounce最適化まで

検索ボックスに入力したキーワードをページ内でハイライト表示する機能は、ユーザーが目的の情報をすばやく見つけるUX改善に直結します。この記事ではjQuery基本実装・mark.js・前後ナビゲーション・URLパラメーターからの自動ハイライト・複数色・debounce最適化まで解説します。

この記事でわかること

  • jQuery + replace() によるシンプルなキーワードハイライト
  • mark.js で既存HTMLタグを壊さずにハイライトする
  • 「前の一致」「次の一致」ナビゲーション + ヒット件数表示
  • URLのクエリパラメーターからキーワードを取得して自動ハイライトする
  • 複数キーワードを異なる色でハイライト
  • debounce でパフォーマンスを最適化する
スポンサーリンク

実装方法の比較

方法 既存タグ保持 複数キーワード ナビ機能 jQuery依存 推奨場面
jQuery replace() △(htmlで取得すれば○) ○(|で連結) 自作 必須 シンプルな実装・jQuery既導入
mark.js ◎(テキストノードのみ) ◎(配列渡し) ◎(自動対応) 任意 本格的なサイト内検索・ブログ
CSS ::highlight × 不要 最新ブラウザのみ・スタイル変更だけ

jQuery による基本実装

<input type="text" id="search-input" placeholder="キーワードを入力...">
<button id="clear-btn">クリア</button>
<span id="match-count"></span>

<div id="content">
  <p>jQueryを使ったWebサイトの検索機能の実装例です。
  JavaScriptの基礎知識があればjQueryを活用して高度な機能を実装できます。</p>
</div>
$(function () {
  // 元のHTMLを保存しておく(既存タグを維持するためhtml()で取得)
  var originalHtml = $("#content").html();

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

  function applyHighlight(keyword) {
    if (!keyword) {
      $("#content").html(originalHtml);
      $("#match-count").text("");
      return;
    }
    var pattern = new RegExp("(" + escapeRegex(keyword) + ")", "gi");
    var highlighted = originalHtml.replace(pattern, "<mark>$1</mark>");
    $("#content").html(highlighted);

    // ヒット件数を表示
    var count = $("mark").length;
    $("#match-count").text(count > 0 ? count + " 件ヒット" : "一致なし");
  }

  $("#search-input").on("input", function () {
    applyHighlight($(this).val().trim());
  });

  $("#clear-btn").on("click", function () {
    $("#search-input").val("");
    applyHighlight("");
  });
});
html() で保存するか text() で保存するかの判断
コンテンツ内に <strong><a> などの既存タグがある場合は html() で保存して html() で復元します。プレーンテキストのみなら text() の方が安全です。詳しくは特定の文字列をHTMLタグで囲む完全ガイドも参照してください。

mark.js ライブラリを使う(推奨)

mark.js はテキストノードのみを操作するため、既存のHTMLタグを破壊しません。jQuery replace() では <a href="..."> の属性値まで誤置換するリスクがありますが、mark.js はその問題を解消します。

<!-- CDN で導入(jQuery版) -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/jquery.mark.min.js"></script>
// npm: npm install mark.js
// import Mark from "mark.js";

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

    // 既存のハイライトを解除 → 新しいキーワードをハイライト
    $("#content").unmark({
      done: function () {
        if (!keyword) return;
        $("#content").mark(keyword, {
          caseSensitive: false,      // 大文字小文字を無視
          separateWordSearch: false, // スペース区切りを単語分割しない
          done: function (total) {
            $("#match-count").text(total > 0 ? total + " 件ヒット" : "一致なし");
          }
        });
      }
    });
  });
});
mark.js は done コールバックでヒット件数を取得できる
mark(keyword, { done: function(total) { ... } }) の形でハイライト完了後の件数を取得できます。検索結果の「N件ヒット」表示に活用してください。また unmark() で全ハイライトを一括解除できます。

「前の一致」「次の一致」ナビゲーション

ハイライト箇所を順番にスクロールで移動するナビゲーションです。ブラウザの Ctrl+F に近いUXを実現します。

<div class="search-bar">
  <input type="text" id="search-input" placeholder="キーワード...">
  <button id="prev-btn">&#9650; 前へ</button>
  <button id="next-btn">&#9660; 次へ</button>
  <span id="match-info"></span>
</div>
<div id="content">...</div>
$(function () {
  var currentIndex = 0;
  var originalHtml = $("#content").html();

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

  function applyHighlight(keyword) {
    currentIndex = 0;
    if (!keyword) {
      $("#content").html(originalHtml);
      $("#match-info").text("");
      return;
    }
    var pattern = new RegExp("(" + escapeRegex(keyword) + ")", "gi");
    $("#content").html(originalHtml.replace(pattern, '<mark class="search-mark">$1</mark>'));
    updateNav();
  }

  function updateNav() {
    var $marks = $(".search-mark");
    var total  = $marks.length;
    if (total === 0) { $("#match-info").text("一致なし"); return; }

    // 現在位置のマークにアクティブクラスを付与
    $marks.removeClass("is-current");
    var $current = $marks.eq(currentIndex).addClass("is-current");

    // 現在位置にスクロール
    $("html, body").animate({ scrollTop: $current.offset().top - 80 }, 200);
    $("#match-info").text((currentIndex + 1) + " / " + total);
  }

  $("#search-input").on("input", function () {
    applyHighlight($(this).val().trim());
  });

  $("#next-btn").on("click", function () {
    var total = $(".search-mark").length;
    if (!total) return;
    currentIndex = (currentIndex + 1) % total; // 最後まで行ったら最初に戻る
    updateNav();
  });

  $("#prev-btn").on("click", function () {
    var total = $(".search-mark").length;
    if (!total) return;
    currentIndex = (currentIndex - 1 + total) % total;
    updateNav();
  });
});
/* ハイライトのCSSスタイル */
.search-mark {
  background-color: #fff176;
  padding: 0 2px;
  border-radius: 2px;
}

/* 現在のナビゲーション位置(アクティブ) */
.search-mark.is-current {
  background-color: #ff9800;
  color: #fff;
  outline: 2px solid #e65100;
}

URLパラメーターから自動ハイライトする

Google検索などからサイトに流入した際、URLに ?q=キーワード?s=キーワード が付いている場合があります。これを取得してページ読み込み時に自動ハイライトすることで、ユーザーが探している情報をすぐに見つけられるUXを実現できます。

$(function () {
  var originalHtml = $("#content").html();

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

  function highlightFromUrl() {
    // URLSearchParams で ?q= または ?s= パラメーターを取得
    var params  = new URLSearchParams(window.location.search);
    var keyword = params.get("q") || params.get("s") || params.get("keyword") || "";
    keyword = keyword.trim();

    if (!keyword) return;

    var pattern = new RegExp("(" + escapeRegex(keyword) + ")", "gi");
    $("#content").html(originalHtml.replace(pattern, "<mark>$1</mark>"));

    // 最初のハイライト箇所にスクロール
    var $first = $("mark").first();
    if ($first.length) {
      $("html, body").animate({ scrollTop: $first.offset().top - 80 }, 300);
    }

    // 検索ボックスにもキーワードをセット
    $("#search-input").val(keyword);
  }

  highlightFromUrl(); // ページ読み込み時に実行
});
サイト内検索(WordPress)と組み合わせる
WordPressのサイト内検索は ?s=キーワード というURLになります。検索結果ページのテンプレートにこのコードを追加すると、検索結果ページでキーワードが自動的にハイライトされます。検索結果ページでなくても、例えば「○○とは?」のリンクから飛んできたページで URLにキーワードを付けてリンクを作れば同様の効果があります。

複数キーワードを異なる色でハイライト

mark.hl-1 { background-color: #fff176; }  /* 黄 */
mark.hl-2 { background-color: #a5d6a7; }  /* 緑 */
mark.hl-3 { background-color: #ef9a9a; }  /* 赤 */
mark[class^="hl-"] { padding: 0 2px; border-radius: 2px; }
$(function () {
  var keywords    = ["jQuery", "JavaScript", "HTML"];
  var originalHtml = $("#content").html();

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

  var result = originalHtml;
  // 長いキーワードを先に処理(短いキーワードが先にマッチして上書きされるのを防ぐ)
  keywords
    .slice().sort(function (a, b) { return b.length - a.length; })
    .forEach(function (kw, i) {
      result = result.replace(
        new RegExp("(" + escapeRegex(kw) + ")", "gi"),
        '<mark class="hl-' + (i + 1) + '">$1</mark>'
      );
    });
  $("#content").html(result);
});
すでにハイライトされた <mark> の中が再度置換されることがある
キーワードを順番に replace() すると、先にハイライトした <mark class="hl-1">jQuery</mark>class 属性の値が次の置換で誤ってハイライトされることがあります。この問題を回避するには mark.js の配列渡しを使うか、複数のパスで処理する際は元のHTMLから毎回やり直してください。

debounce でパフォーマンスを最適化する

テキストが長い場合、input イベントのたびにハイライト処理を走らせると負荷が高くなります。debounce(入力が止まってから一定時間後に実行)で処理を間引きます。

$(function () {
  var originalHtml = $("#content").html();
  var debounceTimer;

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

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

    clearTimeout(debounceTimer); // 前のタイマーをキャンセル

    debounceTimer = setTimeout(function () {
      if (!keyword) {
        $("#content").html(originalHtml);
        return;
      }
      var pattern = new RegExp("(" + escapeRegex(keyword) + ")", "gi");
      $("#content").html(originalHtml.replace(pattern, "<mark>$1</mark>"));
    }, 300); // 300ms 入力がなければ実行
  });
});
debounce と throttle の違い

  • debounce: 最後の入力から一定時間後に実行(検索ハイライトに最適)
  • throttle: 一定時間ごとに最大1回実行(スクロールイベントに最適)

ハイライト更新は「入力が止まってから」実行すれば良いため debounce が適しています。300ms は一般的な目安で、コンテンツが長い場合は500ms程度にすることも検討してください。

まとめ

検索キーワードハイライトの実装方法を用途に応じて選んでください。

  • シンプルな実装: jQuery + replace() + <mark>
  • 既存タグを保持する場合: mark.js(unmark()mark()
  • 前後ナビ: .search-mark クラス + eq() でスクロール
  • URL自動ハイライト: URLSearchParams?q= を取得
  • 複数色: キーワードごとにクラスを変えて CSS で色指定
  • パフォーマンス: debounce(300ms)で処理頻度を制御

関連記事: 特定の文字列をHTMLタグで囲む完全ガイド / リストをリアルタイムに絞り込む完全ガイド / フィルター機能の実装完全ガイド

よくある質問(FAQ)

Qハイライトを解除したいです。
A保存しておいた元のHTMLを $("#content").html(originalHtml) でセットするか、mark.js を使っている場合は $("#content").unmark() で解除できます。$("mark").contents().unwrap() で mark タグのみを取り除く方法もあります。
QAjax や SPA で動的に追加されたコンテンツにも対応したいです。
A動的に追加されたコンテンツに対しては、コンテンツ挿入後に再度ハイライト関数を呼んでください。元HTMLの保存(originalHtml)も挿入後に取り直す必要があります。mark.js も追加後に再度 mark() を呼んでください。MutationObserver でコンテナを監視して自動的に再適用する実装も有効です。
Qスペース区切りで複数キーワードを同時に指定したいです。
Akeyword.split(/\s+/).filter(Boolean) でスペース区切りに分割し、各キーワードをエスケープして | で結合した正規表現を使います:
var pattern = keywords.map(escapeRegex).join("|");
mark.js では配列を直接渡せます:$("#content").mark(["word1", "word2"])
Q入力中にハイライトが点滅して見づらいです。
Adebounce の時間を長め(500ms程度)に設定すると軽減できます。また mark.js の acrossElements: true オプションで要素をまたいだハイライトを安定させることもできます。CSSで mark { transition: background-color 0.15s; } を設定するとなめらかな切り替えになります。
QWordPress のサイト内検索結果でキーワードを自動ハイライトしたいです。
AWordPressの検索URLは ?s=キーワード の形式です。上述の「URLパラメーターから自動ハイライト」のコードを、検索結果ページ(search.phpindex.php)のテンプレートか、子テーマの functions.phpwp_enqueue_script() を使って読み込んでください。params.get("s") でキーワードが取得できます。