FAQ 中級

検索フィルタ付きFAQ

上部の検索ボックスに入力すると、質問がリアルタイムで絞り込まれるFAQ。該当語をハイライトし、件数も表示します。質問数が多いヘルプセンターで、目的の答えに素早く辿り着けます。

#faq#search#filter#help

ライブデモ

使用例(お題: アイドルグループ Sakura)

この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- Sakura:公式ヘルプの検索フィルタ付きFAQ -->
<section class="fqs">
  <div class="fqs-inner">
    <h2 class="fqs-title">ヘルプを検索</h2>
    <div class="fqs-search">
      <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 21l-4.3-4.3M11 18a7 7 0 1 1 0-14 7 7 0 0 1 0 14Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
      <input class="fqs-input" id="fqsInput" type="search" placeholder="キーワードで検索(例:チケット)" aria-label="FAQ検索">
    </div>
    <p class="fqs-count" id="fqsCount">5 件の質問</p>

    <ul class="fqs-list" id="fqsList">
      <li class="fqs-item"><b>チケットの先行抽選はどう申し込みますか?</b><span>ファンクラブ会員ページから申し込めます。</span></li>
      <li class="fqs-item"><b>会員証はどこで確認できますか?</b><span>公式アプリのマイページに表示されます。</span></li>
      <li class="fqs-item"><b>支払い方法を教えてください</b><span>クレジットカード・キャリア決済に対応します。</span></li>
      <li class="fqs-item"><b>グッズの再販はありますか?</b><span>受注期間後、数量限定で再販する場合があります。</span></li>
      <li class="fqs-item"><b>退会するにはどうすればいいですか?</b><span>マイページの設定からいつでも退会できます。</span></li>
    </ul>
    <p class="fqs-empty" id="fqsEmpty" hidden>一致する質問が見つかりませんでした。</p>
  </div>
</section>
CSS
/* Sakura(アイドル):検索フィルタ付きFAQの再スキン */
* { box-sizing: border-box; }
body { margin: 0; font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", system-ui, sans-serif; }

.fqs { width: 100%; min-height: 380px; display: grid; place-content: center; padding: 30px 26px; background: #20111c; color: #f3dde7; }
.fqs-inner { width: min(560px, 94vw); }
.fqs-title { margin: 0 0 16px; font-size: 24px; font-weight: 800; text-align: center; color: #fff; }
.fqs-search { display: flex; align-items: center; gap: 10px; background: #2c1826; border: 1px solid #4a2b3c; border-radius: 12px; padding: 0 14px; }
.fqs-search svg { width: 18px; height: 18px; color: #b07a92; flex: 0 0 auto; }
.fqs-input { flex: 1; font: inherit; font-size: 14px; color: #fff; background: none; border: none; outline: none; padding: 13px 0; }
.fqs-input::placeholder { color: #8a6175; }
.fqs-count { margin: 12px 2px 14px; font-size: 12px; color: #b07a92; }
.fqs-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.fqs-item { background: #2a1623; border: 1px solid #43283a; border-radius: 11px; padding: 13px 16px; transition: opacity .2s ease; }
.fqs-item[hidden] { display: none; }
.fqs-item b { display: block; font-size: 13.5px; color: #f6e3ec; }
.fqs-item span { display: block; margin-top: 4px; font-size: 12.5px; color: #c193a9; }
.fqs-item mark { background: rgba(240,101,149,.45); color: #fff; border-radius: 3px; padding: 0 2px; }
.fqs-empty { margin: 14px 0 0; font-size: 13px; color: #b07a92; text-align: center; }

@media (prefers-reduced-motion: reduce) { .fqs-item { transition: none; } }
JavaScript
// (デモと同じフックを流用)入力でFAQ絞り込み+ハイライト+件数+自動入力
(() => {
  const input = document.getElementById('fqsInput');
  const list = document.getElementById('fqsList');
  const count = document.getElementById('fqsCount');
  const empty = document.getElementById('fqsEmpty');
  if (!input || !list) return;
  const raw = [...list.querySelectorAll('.fqs-item')].map(it => ({ el: it, b: it.querySelector('b'), s: it.querySelector('span'), bt: it.querySelector('b').textContent, st: it.querySelector('span').textContent }));
  const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;');
  function mark(text, q) { if (!q) return esc(text); const i = text.toLowerCase().indexOf(q); if (i < 0) return esc(text); return esc(text.slice(0, i)) + '<mark>' + esc(text.slice(i, i + q.length)) + '</mark>' + esc(text.slice(i + q.length)); }
  function filter(q) {
    q = (q || '').trim().toLowerCase(); let n = 0;
    raw.forEach(r => { const hit = !q || (r.bt + r.st).toLowerCase().includes(q); r.el.hidden = !hit; if (hit) n++; r.b.innerHTML = mark(r.bt, q); r.s.innerHTML = mark(r.st, q); });
    if (count) count.textContent = n + ' 件の質問';
    if (empty) empty.hidden = n !== 0;
  }
  input.addEventListener('input', () => { auto = false; filter(input.value); });
  let auto = !matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (auto) {
    const word = 'チケット'; let i = 0, phase = 'type';
    const tick = () => {
      if (!auto) return;
      if (phase === 'type') { i++; if (i >= word.length) phase = 'hold'; }
      else if (phase === 'hold') phase = 'wait';
      else if (phase === 'wait') { i--; if (i <= 0) phase = 'pause'; else phase = 'wait'; }
      else phase = 'type';
      input.value = word.slice(0, i); filter(input.value);
      setTimeout(tick, phase === 'hold' ? 1600 : (phase === 'pause' ? 1800 : 220));
    };
    setTimeout(tick, 1600);
  }
})();

実装ガイド

使いどころ

質問数が多いヘルプセンターに。検索ボックスに入力すると質問がリアルタイムで絞り込まれ、該当語をハイライト、件数も表示します。

実装時の注意点

入力のたびに質問+回答のテキストを部分一致で判定し、表示/非表示と件数を更新。一致箇所は <mark> で強調します。プレビューでは自動でキーワードを打つ→消すを繰り返します。

対応ブラウザ

input イベント・String 検索は全モダンブラウザ対応。日本語IME中の挙動は実運用で compositionend も考慮すると確実です。

よくある失敗

大量データはクライアント側の全件フィルタが重くなるため、件数が多ければサーバ検索やインデックスを。0件時の代替導線(問い合わせ)を用意。表記ゆれ(ひらがな/カタカナ)対策に正規化を入れると精度が上がります。

応用例

カテゴリ絞り込みの併用、人気質問の初期表示、サジェスト、ヒット0件時の関連リンク提案などに展開できます。

コード

HTML
<!-- 検索フィルタ付きFAQ -->
<section class="fqs">
  <div class="fqs-inner">
    <h2 class="fqs-title">ヘルプを検索</h2>
    <div class="fqs-search">
      <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 21l-4.3-4.3M11 18a7 7 0 1 1 0-14 7 7 0 0 1 0 14Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
      <input class="fqs-input" id="fqsInput" type="search" placeholder="キーワードで検索(例:解約)" aria-label="FAQ検索">
    </div>
    <p class="fqs-count" id="fqsCount">5 件の質問</p>

    <ul class="fqs-list" id="fqsList">
      <li class="fqs-item"><b>無料トライアルはありますか?</b><span>14日間、無料でお試しいただけます。</span></li>
      <li class="fqs-item"><b>解約はどうすればいいですか?</b><span>マイページからいつでも解約できます。</span></li>
      <li class="fqs-item"><b>支払い方法を教えてください</b><span>カード・銀行振込・請求書払いに対応します。</span></li>
      <li class="fqs-item"><b>データのエクスポートはできますか?</b><span>CSV / JSON 形式で出力できます。</span></li>
      <li class="fqs-item"><b>スマホアプリはありますか?</b><span>iOS / Android アプリを提供しています。</span></li>
    </ul>
    <p class="fqs-empty" id="fqsEmpty" hidden>一致する質問が見つかりませんでした。</p>
  </div>
</section>
CSS
* { box-sizing: border-box; }
body { margin: 0; font-family: "Segoe UI", system-ui, -apple-system, sans-serif; }

.fqs { width: 100%; min-height: 380px; display: grid; place-content: center; padding: 30px 26px; background: #0f1320; color: #e7eaf5; }
.fqs-inner { width: min(560px, 94vw); }
.fqs-title { margin: 0 0 16px; font-size: 24px; font-weight: 800; text-align: center; color: #fff; }
.fqs-search { display: flex; align-items: center; gap: 10px; background: #1a2030; border: 1px solid #2a3247; border-radius: 12px; padding: 0 14px; }
.fqs-search svg { width: 18px; height: 18px; color: #7b86a3; flex: 0 0 auto; }
.fqs-input { flex: 1; font: inherit; font-size: 14px; color: #fff; background: none; border: none; outline: none; padding: 13px 0; }
.fqs-input::placeholder { color: #6b7590; }
.fqs-count { margin: 12px 2px 14px; font-size: 12px; color: #8b95b0; }

.fqs-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.fqs-item { background: #161b29; border: 1px solid #232a3d; border-radius: 11px; padding: 13px 16px; transition: opacity .2s ease; }
.fqs-item[hidden] { display: none; }
.fqs-item b { display: block; font-size: 13.5px; color: #eef1f8; }
.fqs-item span { display: block; margin-top: 4px; font-size: 12.5px; color: #97a0ba; }
.fqs-item mark { background: rgba(99,102,241,.4); color: #fff; border-radius: 3px; padding: 0 2px; }
.fqs-empty { margin: 14px 0 0; font-size: 13px; color: #8b95b0; text-align: center; }

@media (prefers-reduced-motion: reduce) { .fqs-item { transition: none; } }
JavaScript
// 入力でFAQをリアルタイム絞り込み+ハイライト+件数。プレビューでは自動入力も実演
(() => {
  const input = document.getElementById('fqsInput');
  const list = document.getElementById('fqsList');
  const count = document.getElementById('fqsCount');
  const empty = document.getElementById('fqsEmpty');
  if (!input || !list) return;
  const items = [...list.querySelectorAll('.fqs-item')];
  const raw = items.map(it => ({ el: it, b: it.querySelector('b'), s: it.querySelector('span'), bt: it.querySelector('b').textContent, st: it.querySelector('span').textContent }));
  const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;');

  function mark(text, q) {
    if (!q) return esc(text);
    const i = text.toLowerCase().indexOf(q);
    if (i < 0) return esc(text);
    return esc(text.slice(0, i)) + '<mark>' + esc(text.slice(i, i + q.length)) + '</mark>' + esc(text.slice(i + q.length));
  }
  function filter(q) {
    q = (q || '').trim().toLowerCase();
    let n = 0;
    raw.forEach(r => {
      const hit = !q || (r.bt + r.st).toLowerCase().includes(q);
      r.el.hidden = !hit;
      if (hit) n++;
      r.b.innerHTML = mark(r.bt, q);
      r.s.innerHTML = mark(r.st, q);
    });
    if (count) count.textContent = n + ' 件の質問';
    if (empty) empty.hidden = n !== 0;
  }
  input.addEventListener('input', () => { auto = false; filter(input.value); });

  // 自動デモ:キーワードを打つ→消す
  let auto = !matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (auto) {
    const word = '解約';
    let i = 0, phase = 'type';
    const tick = () => {
      if (!auto) { return; }
      if (phase === 'type') { i++; if (i >= word.length) phase = 'hold'; }
      else if (phase === 'hold') { phase = 'wait'; }
      else if (phase === 'wait') { i--; if (i <= 0) { phase = 'pause'; } else { phase = 'wait'; } }
      else { phase = 'type'; }
      input.value = word.slice(0, i);
      filter(input.value);
      const delay = phase === 'hold' ? 1600 : (phase === 'pause' ? 1800 : 220);
      setTimeout(tick, delay);
    };
    setTimeout(tick, 1600);
  }
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「検索フィルタ付きFAQ」の効果を追加してください。

# 追加してほしい効果
検索フィルタ付きFAQ(FAQ)
上部の検索ボックスに入力すると、質問がリアルタイムで絞り込まれるFAQ。該当語をハイライトし、件数も表示します。質問数が多いヘルプセンターで、目的の答えに素早く辿り着けます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 検索フィルタ付きFAQ -->
<section class="fqs">
  <div class="fqs-inner">
    <h2 class="fqs-title">ヘルプを検索</h2>
    <div class="fqs-search">
      <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 21l-4.3-4.3M11 18a7 7 0 1 1 0-14 7 7 0 0 1 0 14Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
      <input class="fqs-input" id="fqsInput" type="search" placeholder="キーワードで検索(例:解約)" aria-label="FAQ検索">
    </div>
    <p class="fqs-count" id="fqsCount">5 件の質問</p>

    <ul class="fqs-list" id="fqsList">
      <li class="fqs-item"><b>無料トライアルはありますか?</b><span>14日間、無料でお試しいただけます。</span></li>
      <li class="fqs-item"><b>解約はどうすればいいですか?</b><span>マイページからいつでも解約できます。</span></li>
      <li class="fqs-item"><b>支払い方法を教えてください</b><span>カード・銀行振込・請求書払いに対応します。</span></li>
      <li class="fqs-item"><b>データのエクスポートはできますか?</b><span>CSV / JSON 形式で出力できます。</span></li>
      <li class="fqs-item"><b>スマホアプリはありますか?</b><span>iOS / Android アプリを提供しています。</span></li>
    </ul>
    <p class="fqs-empty" id="fqsEmpty" hidden>一致する質問が見つかりませんでした。</p>
  </div>
</section>

【CSS】
* { box-sizing: border-box; }
body { margin: 0; font-family: "Segoe UI", system-ui, -apple-system, sans-serif; }

.fqs { width: 100%; min-height: 380px; display: grid; place-content: center; padding: 30px 26px; background: #0f1320; color: #e7eaf5; }
.fqs-inner { width: min(560px, 94vw); }
.fqs-title { margin: 0 0 16px; font-size: 24px; font-weight: 800; text-align: center; color: #fff; }
.fqs-search { display: flex; align-items: center; gap: 10px; background: #1a2030; border: 1px solid #2a3247; border-radius: 12px; padding: 0 14px; }
.fqs-search svg { width: 18px; height: 18px; color: #7b86a3; flex: 0 0 auto; }
.fqs-input { flex: 1; font: inherit; font-size: 14px; color: #fff; background: none; border: none; outline: none; padding: 13px 0; }
.fqs-input::placeholder { color: #6b7590; }
.fqs-count { margin: 12px 2px 14px; font-size: 12px; color: #8b95b0; }

.fqs-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.fqs-item { background: #161b29; border: 1px solid #232a3d; border-radius: 11px; padding: 13px 16px; transition: opacity .2s ease; }
.fqs-item[hidden] { display: none; }
.fqs-item b { display: block; font-size: 13.5px; color: #eef1f8; }
.fqs-item span { display: block; margin-top: 4px; font-size: 12.5px; color: #97a0ba; }
.fqs-item mark { background: rgba(99,102,241,.4); color: #fff; border-radius: 3px; padding: 0 2px; }
.fqs-empty { margin: 14px 0 0; font-size: 13px; color: #8b95b0; text-align: center; }

@media (prefers-reduced-motion: reduce) { .fqs-item { transition: none; } }

【JavaScript】
// 入力でFAQをリアルタイム絞り込み+ハイライト+件数。プレビューでは自動入力も実演
(() => {
  const input = document.getElementById('fqsInput');
  const list = document.getElementById('fqsList');
  const count = document.getElementById('fqsCount');
  const empty = document.getElementById('fqsEmpty');
  if (!input || !list) return;
  const items = [...list.querySelectorAll('.fqs-item')];
  const raw = items.map(it => ({ el: it, b: it.querySelector('b'), s: it.querySelector('span'), bt: it.querySelector('b').textContent, st: it.querySelector('span').textContent }));
  const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;');

  function mark(text, q) {
    if (!q) return esc(text);
    const i = text.toLowerCase().indexOf(q);
    if (i < 0) return esc(text);
    return esc(text.slice(0, i)) + '<mark>' + esc(text.slice(i, i + q.length)) + '</mark>' + esc(text.slice(i + q.length));
  }
  function filter(q) {
    q = (q || '').trim().toLowerCase();
    let n = 0;
    raw.forEach(r => {
      const hit = !q || (r.bt + r.st).toLowerCase().includes(q);
      r.el.hidden = !hit;
      if (hit) n++;
      r.b.innerHTML = mark(r.bt, q);
      r.s.innerHTML = mark(r.st, q);
    });
    if (count) count.textContent = n + ' 件の質問';
    if (empty) empty.hidden = n !== 0;
  }
  input.addEventListener('input', () => { auto = false; filter(input.value); });

  // 自動デモ:キーワードを打つ→消す
  let auto = !matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (auto) {
    const word = '解約';
    let i = 0, phase = 'type';
    const tick = () => {
      if (!auto) { return; }
      if (phase === 'type') { i++; if (i >= word.length) phase = 'hold'; }
      else if (phase === 'hold') { phase = 'wait'; }
      else if (phase === 'wait') { i--; if (i <= 0) { phase = 'pause'; } else { phase = 'wait'; } }
      else { phase = 'type'; }
      input.value = word.slice(0, i);
      filter(input.value);
      const delay = phase === 'hold' ? 1600 : (phase === 'pause' ? 1800 : 220);
      setTimeout(tick, delay);
    };
    setTimeout(tick, 1600);
  }
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。