【PR】を含みます。

フロントエンド

【JavaScript】サジェスト機能を自作する方法と実装例

JavaScript サジェスト機能を自作する方法と実装例

Webサイトで検索機能を提供する際に欠かせないのが「サジェスト(オートコンプリート)機能」です。

この記事では、JavaScriptを使ってサジェスト機能をライブラリなしで自作する方法を、実装例とともにわかりやすく解説します。

サジェスト機能とは?

サジェスト機能とは、ユーザーが検索フォームに文字を入力すると、入力内容に応じた候補を自動的に表示する機能です。

Google検索やAmazonなど、あらゆるWebサービスで利用されており、以下のようなメリットがあります。

  • 入力補助による利便性向上
  • 誤字の予防
  • 検索精度の向上

JavaScriptでサジェスト機能を実装する仕組み

自作する場合、主に次の要素が必要です。

  • 入力欄(<input>
  • 候補リスト表示用の要素(<ul>など)
  • イベントリスナーでユーザー入力を検知
  • 候補の配列と一致判定処理
  • DOMへの候補表示

【サンプル】サジェスト

例:りんご、みかん、ぶどう、バナナ、レモンのいずれかを入力すると、候補が表示されます。

      【実装コード】サジェスト

      HTMLの基本構造

      以下は、サジェストのHTML構造です。

      Copyをクリックするとコピーできます。

      HTML
      Copy
      <div class="suggest">
        <input name="suggest-input1" class="suggest-input" type="text" autocomplete="off" placeholder="果物の名前を入力">
        <ul class="suggest-list"></ul>
      </div>
      <div class="suggest">
        <input name="suggest-input2" class="suggest-input" type="text" autocomplete="off" placeholder="果物の名前を入力">
        <ul class="suggest-list"></ul>
      </div>

      CSSで見た目を整える

      入力エリアとサジェストリストのデザインは以下のように定義します。

      Copyをクリックするとコピーできます。

      CSS
      Copy
      .suggest {
        position: relative;
        margin-bottom: 1em;
      }
      .suggest-input {
        width: 100%;
        max-width: 300px;
        margin: 0;
        padding: 0.5em 2em 0.5em 0.8em;
        font-size: 14px;
        background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2215%22%20height%3D%2215%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Ccircle%20cx%3D%2211%22%20cy%3D%2211%22%20r%3D%227%22%20stroke%3D%22%23666%22%20stroke-width%3D%222%22/%3E%3Cline%20x1%3D%2216.65%22%20y1%3D%2216.65%22%20x2%3D%2222%22%20y2%3D%2222%22%20stroke%3D%22%23666%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22/%3E%3C/svg%3E");
        background-position: right 0.5em center;
        background-size: 1.2em 1.2em;
        background-repeat: no-repeat;
        border: 1px solid #ccc;
        border-radius: 4px;
      }
      .suggest-input:focus,
      .suggest-input:focus-visible {
        outline: none;
        border-color: #f09896;
        box-shadow: 0 0 4px 1px #f09896;
      }
      .suggest-list {
        display: none;
        position: absolute;
        left: 0;
        z-index: 100;
        max-height: 100px;
        margin: 0;
        padding: 0;
        overflow-y: auto;
        list-style: none;
        background: #fff;
        border: 1px solid #ccc;
        border-radius: 4px;
      }
      .suggest-item {
        padding: 0.3em 0.6em;
        font-size: 14px;
        cursor: pointer;
      }
      .suggest-item:hover {
        background: #ffebea;
      }
      .suggest-item-active {
        background: #ffebea;
      }

      JavaScriptで機能を実装する

      Copyをクリックするとコピーできます。

      JavaScript
      Copy
      document.addEventListener('DOMContentLoaded', function() {
        const suggestions = ['りんご1', 'りんご2', 'りんご3', 'りんご4', 'りんご5', 'みかん', 'ぶどう', 'バナナ', 'レモン'];
        // サジェストリストのクラス名
        const listClass = 'suggest-list';
        // 全てのサジェスト入力欄に対して処理
        document.querySelectorAll('.suggest-input').forEach(inputEl => {
          // 入力欄と同じコンテナ内にあるul要素を取得
          const suggestListEl = inputEl.closest('.suggest').querySelector(`.${listClass}`);
          let currentIndex = -1; // 選択中の候補のインデックス
          let isComposing = false; // 日本語入力中かどうか
          /**
          * サジェストリストの位置を調整
          * - 入力欄の位置を取得して、画面下に収まらない場合は上に表示
          */
          function adjustListPosition() {
            if (!suggestListEl) return;
            // 親基準の座標
            const inputTop = inputEl.offsetTop;
            const inputHeight = inputEl.offsetHeight;
            const inputWidth = inputEl.offsetWidth;
            const listHeight = suggestListEl.offsetHeight;
            const inputRect = inputEl.getBoundingClientRect();
            const windowHeight = window.innerHeight;
            const margin = 3; // 余白(px)
            const spaceBelow = windowHeight - inputRect.bottom;
            // 上下の表示位置を判定
            const top = spaceBelow >= listHeight + margin
              ? inputTop + inputHeight + margin // 下に表示
              : inputTop - listHeight - margin; // 上に表示
            // スタイル適用(親基準)
            suggestListEl.style.top = `${top}px`;
            suggestListEl.style.width = `${inputWidth}px`;
          }
          /**
          * 入力に応じたサジェスト候補を表示
          */
          function showSuggestions() {
            removeSuggestions(); // 候補をリセット
            currentIndex = -1; // 新しく表示するときは選択リセット
            const inputValue = inputEl.value.trim().toLowerCase();
            if (!inputValue) {
              suggestListEl.style.display = 'none';
              return;
            }
            // 候補を抽出
            const matched = suggestions.filter(item => item.toLowerCase().startsWith(inputValue));
            if (!matched.length) {
              suggestListEl.style.display = 'none';
              return;
            }
            // 候補をリストに追加
            matched.forEach(item => {
              const li = document.createElement('li');
              li.textContent = item;
              li.classList.add('suggest-item');
              // クリックで選択
              li.addEventListener('click', () => {
                inputEl.value = item;
                removeSuggestions();
              });
              suggestListEl.appendChild(li);
            });
            suggestListEl.style.display = 'block';
            adjustListPosition();
          }
          /**
          * サジェスト候補を全て削除して非表示にする
          */
          function removeSuggestions() {
            suggestListEl.innerHTML = '';
            suggestListEl.style.display = 'none';
          }
          function closeAllSuggestions() {
            document.querySelectorAll(`.${listClass}`).forEach(list => {
              list.innerHTML = '';
              list.style.display = 'none';
            });
          }
          function moveSelection(direction) {
            const items = suggestListEl.querySelectorAll('.suggest-item');
            if (!items.length) return;
            // 現在の選択を解除
            if (currentIndex >= 0) {
              items[currentIndex].classList.remove('suggest-item-active');
            }
            // インデックス更新
            if (direction === 'down') {
              currentIndex = (currentIndex + 1) % items.length;
            } else if (direction === 'up') {
              currentIndex = (currentIndex - 1 + items.length) % items.length;
            }
            // 新しい選択を適用
            items[currentIndex].classList.add('suggest-item-active');
            items[currentIndex].scrollIntoView({block: 'nearest'});
          }
          // 日本語入力開始
          inputEl.addEventListener('compositionstart', () => {
            isComposing = true;
          });
          // 日本語入力確定
          inputEl.addEventListener('compositionend', () => {
            isComposing = false;
            showSuggestions(); // 確定後にも更新
          });
          // 入力時にサジェスト表示
          inputEl.addEventListener('input', () => {
            closeAllSuggestions(); // 他の入力欄の候補を閉じる
            showSuggestions();
          });
          // フォーカス時にも入力値があればサジェスト表示
          inputEl.addEventListener('focus', () => {
            closeAllSuggestions(); // 他の入力欄の候補を閉じる
            if (inputEl.value.trim().length > 0) {
              showSuggestions();
            }
          });
          inputEl.addEventListener('keydown', (e) => {
            const items = suggestListEl.querySelectorAll('.suggest-item');
            if (!items.length) return;
             // 日本語変換中は矢印/Enterで候補操作しない
             if (isComposing) return;
            if (e.key === 'ArrowDown' || e.key === 'Tab') {
              e.preventDefault(); // デフォルトのTabフォーカス移動を止める
              moveSelection('down');
            } else if (e.key === 'ArrowUp') {
              e.preventDefault();
              moveSelection('up');
            } else if (e.key === 'Enter') {
              if (currentIndex >= 0) {
                e.preventDefault();
                inputEl.value = items[currentIndex].textContent;
                removeSuggestions();
              }
            }
          });
          // 入力欄・サジェスト以外をクリックしたら非表示
          document.addEventListener('click', (e) => {
            if (!e.target.closest('.suggest-input') && !e.target.closest(`.${listClass}`)) {
              closeAllSuggestions(); // 他の入力欄の候補を閉じる
            }
          });
          // ウィンドウサイズ変更・スクロールで位置再計算
          window.addEventListener('resize', adjustListPosition);
          window.addEventListener('scroll', adjustListPosition);
        });
      });

      よくあるトラブルと対処法

      日本語入力中にイベントが2回発火する

      compositionstart/compositionendを使って制御しましょう。

      候補リストが消えない

      inputのblurイベント、またはdocumentのclickイベントを使って非表示処理を追加しましょう。

      候補が重複して表示される

      listの中身を毎回クリアしましょう。(innerHTML = ''

      サジェスト実装時の注意点

      • 候補数が多すぎる場合はパフォーマンスに注意
      • アクセシビリティ(キーボード操作・ARIA属性など)への配慮
      • スマホ対応やタッチ操作への対応

      まとめ

      サジェスト機能は、JavaScriptだけで簡単に実装できます。

      基本構造を理解したうえで、UXを意識した細かなチューニングを行うことで、実用的な機能に仕上がります。

      ライブラリに頼らず自作することで、イベント処理やDOM操作の理解も深まります。

      今後は以下のような改良も可能です。

      • fetchを使った外部APIとの連携
        候補をサーバー側から取得することで、より柔軟に対応できます。
      • アクセシビリティ強化

      ぜひこの記事を参考に、あなたのWebアプリにも導入してみてください。

      -フロントエンド
      -