TextBox 4 — テキストボックス オートコンプリート

フォーム 中級

このコンポーネントについて

テキストボックスの入力に連動して候補リストをドロップダウン表示し、クリックまたはキーボード操作で素早く値を選択できるオートコンプリートUIの実装例です。

ドロップダウンリスト(<select>)は選択肢が数十件を超えると使いにくくなりますが、オートコンプリートなら文字を数文字打つだけで候補が絞り込まれ、長大なリストをスクロールせずに選択できます。 このページでは東京都の市区町村(62件)を候補データとして使い、入力するたびに部分一致で絞り込む動作を体感できます。

候補データはJSONファイルで管理するため、差し替えるだけで住所・商品名・社員名など任意のリストに応用できます。 HTML・CSS・バニラJavaScriptのみで実装しており、外部ライブラリは不要です。

  • インクリメンタルサーチ(部分一致) — 入力のたびにJSONデータを絞り込み、マッチした候補をドロップダウン表示する。「田」と入力すれば「千代田区」「大田区」「東大和市」などが即座に出る
  • キーボードナビゲーション — ↑↓キーで候補を移動、Enterで確定、Escapeでドロップダウンを閉じる。マウス不要で完結できる
  • クリック選択 — 候補をクリックしても値を確定できる
  • フォーカス外れで閉じる — 入力欄以外の場所をクリックするとドロップダウンが閉じる
  • JSONデータ管理 — 候補データを fetch() で外部JSONから取得する。データの差し替えがHTML・JSを書き換えずに済む

実装のポイント・注意点

候補クリックは click ではなく mousedown で検知します。 入力欄のフォーカスが外れる(blur)タイミングとクリックが競合するため、click を使うとドロップダウンが先に閉じて選択できないことがあります。mousedown はblurより先に発火するため、選択を確実に処理できます。e.preventDefault() で入力欄のフォーカスを保ちつつ選択を確定します。

fetch() はローカルファイルで動作しません。 file:// プロトコルではCORSエラーになるため、VS CodeのLive Server拡張機能や npx serve などで簡易サーバーを起動してから開いてください。

キーボードナビの activeIndex-1 を初期値にします。 -1のときはEnterを押しても選択しないため、「入力してそのままEnterでフォームを誤送信」するリスクを防げます。

候補リストのDOM生成は createElement + textContent で行います。 innerHTML に変数を結合するとXSSリスクが生じるため、テキストの挿入はすべて textContent を使います。

HTML・CSS・バニラJavaScriptのみで実装しており、フレームワーク不要でコピペすぐに動きます。

デモ

    サンプルソース

    4つのファイルを同じフォルダに保存し、簡易サーバーで index.html を開くと動作確認できます。
    ファイル構成:index.html / style.css / script.js / data/data.jsondata フォルダを作って中に配置)
    保存時の文字コードは UTF-8 を指定してください。fetch()file:// では動作しないため、VS Code の Live Server 等で開いてください。

    <!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="tb4-autocomplete">
      <label class="tb4-label" for="tb4-input">市区町村</label>
      <div class="tb4-input-wrap">
        <input
          type="text"
          id="tb4-input"
          class="tb4-input"
          placeholder="市区町村を入力"
          autocomplete="off"
          aria-autocomplete="list"
          aria-haspopup="listbox"
        >
        <!-- 候補リスト(JSで動的生成) -->
        <ul class="tb4-dropdown" id="tb4-dropdown" role="listbox" aria-label="市区町村の候補"></ul>
      </div>
      <div class="tb4-result" id="tb4-result" aria-live="polite"></div>
    </div>
    
    <script src="./script.js"></script>
    </body>
    </html>
    /* === オートコンプリート サンプル ===
       :root の変数を書き換えると色を一括変更できます */
    :root {
      --color-accent: #2B7FE8;
      --color-text:   #1A2332;
      --color-border: #D0D7E0;
      --color-bg:     #F4F6F9;
    }
    * { box-sizing: border-box; }
    body { font-family: sans-serif; color: var(--color-text); padding: 24px; max-width: 480px; }
    
    /* ラベル */
    .tb4-label {
      display: block;
      margin-bottom: 6px;
      font-size: 13px;
      font-weight: 700;
      color: #5A6A7A;
    }
    
    /* 入力欄ラッパー(ドロップダウンの絶対配置基点) */
    .tb4-input-wrap { position: relative; }
    
    /* テキスト入力欄 */
    .tb4-input {
      width: 100%;
      padding: 10px 14px;
      border: 1.5px solid var(--color-border);
      border-radius: 8px;
      font-size: 14px;
      color: var(--color-text);
      background: #fff;
      outline: none;
      transition: border-color 0.2s, box-shadow 0.2s;
    }
    .tb4-input:focus {
      border-color: var(--color-accent);
      box-shadow: 0 0 0 3px rgba(43, 127, 232, 0.12);
    }
    
    /* 候補ドロップダウン */
    .tb4-dropdown {
      display: none; /* JSで表示切り替え */
      position: absolute;
      top: calc(100% + 4px);
      left: 0;
      right: 0;
      max-height: 240px;
      overflow-y: auto;
      list-style: none;
      margin: 0;
      padding: 4px 0;
      background: #fff;
      border: 1.5px solid var(--color-border);
      border-radius: 8px;
      box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
      z-index: 100;
    }
    
    /* 候補アイテム */
    .tb4-dropdown-item {
      padding: 10px 14px;
      font-size: 14px;
      cursor: pointer;
      color: var(--color-text);
      transition: background 0.1s;
    }
    .tb4-dropdown-item:hover { background: var(--color-bg); }
    
    /* キーボードナビ時のハイライト */
    .tb4-dropdown-item.active {
      background: var(--color-accent);
      color: #fff;
    }
    
    /* 選択結果表示エリア */
    .tb4-result { margin-top: 14px; font-size: 14px; }
    .tb4-result.has-value {
      padding: 12px 16px;
      border: 1.5px solid var(--color-border);
      border-radius: 8px;
      background: #fff;
    }
    
    /* スマホ対応 */
    @media (max-width: 480px) {
      body { padding: 16px; }
    }
    var cityData    = [];
    var activeIndex = -1;
    
    var input    = document.getElementById('tb4-input');
    var dropdown = document.getElementById('tb4-dropdown');
    var result   = document.getElementById('tb4-result');
    
    // ページ読み込み時にJSONを1回だけ取得する
    // ※ fetch() は file:// では動作しないため、簡易サーバーで開いてください
    (function () {
      fetch('./data/data.json')
        .then(function (res) { return res.json(); })
        .then(function (data) { cityData = data; })
        .catch(function (err) {
          console.error('data.json の読み込みに失敗しました。data/ フォルダを確認してください。', err);
        });
    })();
    
    // 入力のたびに候補を絞り込んで表示する
    input.addEventListener('input', function () {
      var query = input.value;
      if (!query) { closeDropdown(); return; }
    
      var matches = cityData.filter(function (name) {
        return name.indexOf(query) !== -1;
      });
      if (matches.length === 0) { closeDropdown(); return; }
    
      renderDropdown(matches);
    });
    
    // キーボードナビゲーション
    input.addEventListener('keydown', function (e) {
      var items = dropdown.querySelectorAll('.tb4-dropdown-item');
      if (!items.length) return;
    
      if (e.key === 'ArrowDown') {
        e.preventDefault();
        if (activeIndex < items.length - 1) activeIndex++;
        updateActive(items);
      } else if (e.key === 'ArrowUp') {
        e.preventDefault();
        if (activeIndex > 0) activeIndex--;
        updateActive(items);
      } else if (e.key === 'Enter') {
        e.preventDefault();
        if (activeIndex >= 0 && items[activeIndex]) {
          selectCity(items[activeIndex].textContent);
        }
      } else if (e.key === 'Escape') {
        closeDropdown();
      }
    });
    
    // 候補リストを描画する
    function renderDropdown(matches) {
      dropdown.innerHTML = '';
      activeIndex = -1;
      for (var i = 0; i < matches.length; i++) {
        var li = document.createElement('li');
        li.className = 'tb4-dropdown-item';
        li.setAttribute('role', 'option');
        li.textContent = matches[i]; // textContent でXSS対策
        (function (name) {
          // mousedown: blur より先に発火するため click より確実に選択できる
          li.addEventListener('mousedown', function (e) {
            e.preventDefault(); // フォーカスを保持したまま選択確定
            selectCity(name);
          });
        })(matches[i]);
        dropdown.appendChild(li);
      }
      dropdown.style.display = 'block';
    }
    
    // ハイライト中の候補を更新する
    function updateActive(items) {
      for (var i = 0; i < items.length; i++) {
        items[i].classList.remove('active');
      }
      if (activeIndex >= 0) {
        items[activeIndex].classList.add('active');
        items[activeIndex].scrollIntoView({ block: 'nearest' });
      }
    }
    
    // 候補を確定して入力欄にセットする
    function selectCity(name) {
      input.value = name;
      closeDropdown();
      result.textContent = '選択した市区町村: ' + name;
      result.className = 'tb4-result has-value';
    }
    
    // ドロップダウンを閉じる
    function closeDropdown() {
      dropdown.style.display = 'none';
      dropdown.innerHTML = '';
      activeIndex = -1;
    }
    
    // 入力欄以外をクリックしたら閉じる
    document.addEventListener('click', function (e) {
      if (!e.target.closest('.tb4-autocomplete')) {
        closeDropdown();
      }
    });

    data フォルダを作成し、data.json として保存してください。

    [
      "千代田区", "中央区", "港区", "新宿区", "文京区",
      "台東区", "墨田区", "江東区", "品川区", "目黒区",
      "大田区", "世田谷区", "渋谷区", "中野区", "杉並区",
      "豊島区", "北区", "荒川区", "板橋区", "練馬区",
      "足立区", "葛飾区", "江戸川区",
      "八王子市", "立川市", "武蔵野市", "三鷹市", "青梅市",
      "府中市", "昭島市", "調布市", "町田市", "小金井市",
      "小平市", "日野市", "東村山市", "国分寺市", "国立市",
      "福生市", "狛江市", "東大和市", "清瀬市", "東久留米市",
      "武蔵村山市", "多摩市", "稲城市", "羽村市", "あきる野市",
      "西東京市",
      "瑞穂町", "日の出町", "奥多摩町",
      "大島町", "利島村", "新島村", "神津島村",
      "三宅村", "御蔵島村", "八丈町", "青ヶ島村", "小笠原村"
    ]

    AI用プロンプト

    プロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。

    ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。候補データの変更や絞り込み条件の変更など、要件を追記して使うのがおすすめです。

    ※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。

    💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。

    # テキストボックス(オートコンプリート)作成依頼
    
    ## 概要
    テキスト入力に連動して候補リストをドロップダウン表示し、クリックまたはキーボードで値を選択できるオートコンプリート付きテキストボックスを実装してください。
    候補データは外部JSONファイルを fetch で取得して使います。
    
    ## 要件
    - ページ読み込み時に fetch でJSONファイル(文字列配列)を取得し、cityData 変数に保持する
    - テキスト入力のたびに cityData を部分一致でフィルタリングし、ドロップダウンに候補を表示する
    - 入力が空またはマッチ0件のときはドロップダウンを閉じる
    - キーボードナビゲーション対応(↓↑で候補移動、Enterで確定、Escapeで閉じる)
    - ↑↓のループなし:末尾候補でさらに↓を押しても変化しない。先頭候補で↑を押しても変化しない
    - activeIndex の初期値は -1。-1 のとき Enter を押しても選択しない(フォームの誤送信防止)
    - 候補をクリック(mousedown)しても選択できる
    - mousedown を使う理由:blur より先に発火するため、ドロップダウンが先に閉じて選択できない問題を回避できる
    - 入力欄以外をクリックするとドロップダウンが閉じる(document の click イベントで検知)
    - 選択後は入力欄に値がセットされ、下部に「選択した市区町村: ◯◯」と表示される
    
    ## JSONデータ仕様
    ファイル名: data/data.json(HTMLと同じフォルダの data/ 配下に配置)
    
    [
      "千代田区", "中央区", "港区", "新宿区", "文京区",
      "台東区", "墨田区", "江東区", "品川区", "目黒区",
      "大田区", "世田谷区", "渋谷区", "中野区", "杉並区",
      "豊島区", "北区", "荒川区", "板橋区", "練馬区",
      "足立区", "葛飾区", "江戸川区",
      "八王子市", "立川市", "武蔵野市", "三鷹市", "青梅市",
      "府中市", "昭島市", "調布市", "町田市", "小金井市",
      "小平市", "日野市", "東村山市", "国分寺市", "国立市",
      "福生市", "狛江市", "東大和市", "清瀬市", "東久留米市",
      "武蔵村山市", "多摩市", "稲城市", "羽村市", "あきる野市",
      "西東京市",
      "瑞穂町", "日の出町", "奥多摩町",
      "大島町", "利島村", "新島村", "神津島村",
      "三宅村", "御蔵島村", "八丈町", "青ヶ島村", "小笠原村"
    ]
    
    ## 技術仕様
    - HTML / CSS / バニラJavaScript で実装
    - 外部ライブラリ:なし
    - レスポンシブ対応:必要
    
    ## 動作詳細
    fetch でJSONを取得後、input イベントで cityData.filter(name => name.indexOf(query) !== -1) で部分一致絞り込み。
    候補の <li> 要素は innerHTML に変数を入れず、createElement + textContent で生成する(XSS対策)。
    キーボードナビは activeIndex 変数で管理し、対象の <li> に .active クラスを付けてスタイルを変える。
    document の click イベントで入力欄外クリックを検知して closeDropdown() を呼ぶ(e.target.closest('.tb4-autocomplete') で判定)。
    fetch() は file:// では動作しないため、簡易サーバー(Live Server 等)で開く必要がある旨をコメントに記載する。
    
    ## 出力形式
    HTML・CSS・JavaScriptを分けて出力してください。
    JSONファイル(data/data.json)も合わせて出力してください。
    各ファイルは単独でコピー&ペーストして使えるよう記述してください。