ハイライト検索(Highlight Search)— テキスト強調
このコンポーネントについて
テキスト内のキーワードを入力すると、マッチする箇所をリアルタイムに <mark> タグで強調表示するUIです。
社内マニュアル・規程・FAQ・ドキュメントビューワーなど、長文テキストの中から特定のキーワードを素早く見つけたい場面で活躍します。
マッチ件数をリアルタイムで表示するため、「何件ヒットしたか」が一目で分かります。
大文字小文字を区別するかどうかをトグルで切り替えられるため、英字が含まれる文書の検索にも対応できます。
- リアルタイムハイライト — キーワード入力と同時にマッチ箇所を
<mark>で強調表示する。Enterキー不要 - マッチ件数表示 — 「3件ヒット」のように件数をリアルタイム更新する。キーワードが空または0件の場合は非表示
- 大文字小文字無視トグル — チェックボックスで
iフラグのオン/オフを切り替える。デフォルトはオン(区別しない) - XSS安全な実装 —
innerHTMLに変数を直接代入せず、createTextNode/createElement('mark')でDOMを安全に組み立てる - 正規表現特殊文字エスケープ — ユーザー入力に
.*?等が含まれても正規表現エラーにならない
実装のポイント・注意点
ハイライトの実装で最も注意すべきなのはXSS対策です。よく見かける innerHTML に <mark>キーワード</mark> を差し込む方法は、テキスト中に < > & が含まれるとHTMLとして解釈されてしまいます。このサンプルでは document.createTextNode() と createElement('mark') を使ったDOMの直接組み立てにより、ユーザーの入力内容を安全に扱います。
元テキストはDOMではなくJavaScriptの定数 SAMPLE_TEXT で管理しています。一度 mark タグが混入したDOMからプレーンテキストを取り出そうとすると mark タグのテキストが混入する恐れがあるため、毎回この定数を参照してフルレンダリングする方式を採用しています。
ユーザーが . * ( などの正規表現特殊文字を入力した場合も壊れないよう、keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') でエスケープ処理を行っています。この処理を省略すると、特殊文字の入力でJavaScriptエラーが発生したり、意図しない範囲がマッチしたりする問題が起きます。
HTML・CSS・バニラJavaScriptのみで実装しており、フレームワーク不要でコピペすぐに動きます。
デモ
サンプルソース
3つのファイルを同じフォルダに保存し、index.html をブラウザで開くとすぐに動作確認できます。
ファイル名:index.html / style.css / script.js
— 保存時の文字コードは UTF-8 を指定してください(Shift-JISだと日本語が文字化けします)。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ハイライト検索 サンプル</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div class="hl-wrap">
<div class="hl-search-bar">
<input class="hl-input" id="hl-input" type="text" placeholder="キーワードを入力...">
<span class="hl-count" id="hl-count" hidden></span>
</div>
<label class="hl-option">
<input type="checkbox" id="hl-ignore-case" checked>
大文字小文字を区別しない
</label>
<div class="hl-text" id="hl-text"></div>
</div>
<script src="./script.js"></script>
</body>
</html>
:root {
--hl-border: #D0D7E0;
--hl-mark-bg: #FFEC6E;
--hl-text-color: #1a2233;
--hl-count-color: #888;
}
*, *::before, *::after { box-sizing: border-box; }
body { font-family: sans-serif; padding: 24px; background: #f0f2f5; }
.hl-wrap {
max-width: 640px;
margin: 0 auto;
}
.hl-search-bar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.hl-input {
flex: 1;
padding: 8px 12px;
border: 1px solid var(--hl-border);
border-radius: 6px;
font-size: 14px;
font-family: inherit;
outline: none;
transition: border-color 0.15s;
}
.hl-input:focus {
border-color: #3B82F6;
box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
}
/* hidden属性がflex等で上書きされないよう強制非表示 */
[hidden] { display: none !important; }
.hl-count {
font-size: 13px;
color: var(--hl-count-color);
white-space: nowrap;
}
.hl-option {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--hl-text-color);
margin-bottom: 12px;
cursor: pointer;
user-select: none;
}
.hl-text {
padding: 16px;
border: 1px solid var(--hl-border);
border-radius: 6px;
font-size: 14px;
line-height: 1.9;
white-space: pre-wrap;
word-break: break-all;
min-height: 200px;
background: #fff;
color: var(--hl-text-color);
}
mark {
background: var(--hl-mark-bg);
color: inherit;
border-radius: 2px;
padding: 0 1px;
}
// ハイライト対象のテキスト(元データとして保持する)
var SAMPLE_TEXT = '【情報セキュリティ規程】\n\n第1条(目的)\nこの規程は、〇〇株式会社(以下「当社」という)における情報資産の適切な管理および保護を目的とする。\n\n第2条(適用範囲)\nこの規程は、当社の全従業員に適用する。外部委託先についても、契約に基づき同等の管理を求める。\n\n第3条(情報資産の分類)\n情報資産は、その重要度に応じて「機密情報」「社外秘情報」「公開情報」の3区分に分類する。\n取り扱い担当者は、各区分の基準に従い適切に管理しなければならない。\n\n第4条(アクセス管理)\n情報資産へのアクセスは、業務上必要な範囲に限定する。\nアクセス権限は、上長の承認を得た上で情報システム部門が付与する。\n不要になったアクセス権限は速やかに削除しなければならない。\n各システムへのログインには User ID とパスワードを使用する。\nシステムによっては「user id」と小文字表記されることがあるが、同義とみなす。\n\n第5条(インシデント対応)\n情報漏えい・不正アクセス等のセキュリティインシデントが発生または発覚した場合は、\n直ちに情報システム部門および管理職に報告しなければならない。\n報告を受けた管理職は、速やかに対策を講じ、被害の拡大を防止しなければならない。\n\n第6条(パスワード管理)\nPassword は8文字以上とし、英大文字・英小文字・数字を含めること。\npassword の変更は90日ごとに行い、過去3回分と同じものは使用できない。\n\n第7条(罰則)\nこの規程に違反した場合は、就業規則の定めに従い処分を行うことがある。';
// 正規表現の特殊文字をエスケープする(.や*などをそのまま検索できるようにする)
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// テキストをレンダリングする(キーワードがあればmark強調、なければプレーン表示)
function renderText(keyword) {
var container = document.getElementById('hl-text');
var countEl = document.getElementById('hl-count');
var ignoreCase = document.getElementById('hl-ignore-case').checked;
// コンテナをクリア
container.innerHTML = '';
// キーワードが空なら全文をプレーンテキストで表示して終了
if (!keyword) {
container.appendChild(document.createTextNode(SAMPLE_TEXT));
countEl.setAttribute('hidden', '');
return;
}
// 正規表現を組み立てる(大文字小文字フラグ切り替え)
var flags = ignoreCase ? 'gi' : 'g';
var re = new RegExp(escapeRegExp(keyword), flags);
var count = 0;
var lastIndex = 0;
var match;
// マッチを1件ずつ処理してDOMを組み立てる
while ((match = re.exec(SAMPLE_TEXT)) !== null) {
// マッチ前のテキスト部分をテキストノードで追加
if (match.index > lastIndex) {
container.appendChild(document.createTextNode(SAMPLE_TEXT.slice(lastIndex, match.index)));
}
// マッチ部分をmarkタグで追加
var mark = document.createElement('mark');
mark.textContent = match[0];
container.appendChild(mark);
lastIndex = re.lastIndex;
count++;
}
// 最後のマッチ以降の残りテキストを追加
if (lastIndex < SAMPLE_TEXT.length) {
container.appendChild(document.createTextNode(SAMPLE_TEXT.slice(lastIndex)));
}
// 件数表示を更新する
if (count > 0) {
countEl.textContent = count + '件ヒット';
countEl.removeAttribute('hidden');
} else {
countEl.textContent = '0件ヒット';
countEl.removeAttribute('hidden');
}
}
// リセット処理
function resetDemo() {
document.getElementById('hl-input').value = '';
document.getElementById('hl-ignore-case').checked = true;
renderText('');
}
// イベント登録
document.getElementById('hl-input').addEventListener('input', function() {
renderText(this.value);
});
document.getElementById('hl-ignore-case').addEventListener('change', function() {
renderText(document.getElementById('hl-input').value);
});
// 初期表示
renderText('');
AI用プロンプト
以下のプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。
ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。ライブラリ指定や対象テキストの変更など、要件を追記して使うのがおすすめです。
※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。
💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。
# ハイライト検索(テキスト強調) 作成依頼
## 概要
テキスト内のキーワードを入力すると、マッチする箇所をリアルタイムに `<mark>` タグで強調表示するUIを実装してください。
社内マニュアル・規程・ドキュメントビューワーでの利用を想定しています。
## 要件
- 検索ボックスへの入力と同時にハイライトをリアルタイム更新する(Enterキー不要)
- マッチ件数を「◯件ヒット」の形式でリアルタイム表示する。キーワードが空または0件の場合は非表示にする
- 「大文字小文字を区別しない」チェックボックスを設置する。デフォルトはオン(区別しない)
- 「リセット」ボタンで検索ボックスを空にし、ハイライトを解除し、チェックボックスを初期状態に戻す
## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:不要(max-width: 640px 程度で中央寄せ)
## テキスト管理
- サンプルテキストは JavaScript の定数 `SAMPLE_TEXT` で管理する
- DOM 内には直接テキストを書かず、JS の `renderText(keyword)` 関数で毎回再描画する
- サンプルテキスト:情報セキュリティ規程(第1条〜第6条、「管理」「情報」「承認」「アクセス」が複数回登場するもの)
## ハイライト実装仕様
- 検索キーワードを正規表現の特殊文字に対してエスケープしてから RegExp に渡す
- エスケープ: `keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')`
- チェックボックスの状態に応じてフラグを `'gi'`(区別しない)/ `'g'`(区別する)で切り替える
- `RegExp.exec()` ループと `lastIndex` を使って文字列を走査する
- テキスト部分は `document.createTextNode()` で生成する
- マッチ部分は `createElement('mark')` + `textContent` で生成する
- すべてのノードを `appendChild` でコンテナに追加する(`innerHTML` に変数を代入しない)
## XSSセキュリティ要件(重要)
- `innerHTML` にユーザー入力テキストを直接代入しない
- テキスト部分は `document.createTextNode()` で生成する
- マッチ部分は `createElement('mark')` + `textContent` で生成する
## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。