オートコンプリートとは

入力欄に文字を打つたびに、候補の一覧を下に出して選ばせる仕組み。クリックまたは矢印キーで確定する。

概要

入力欄に文字を打つたびに、それに合う候補の一覧を欄の下に表示し、クリックまたは矢印キーと Enter で選ばせる仕組みです。都道府県名や商品名のように候補があらかじめ決まっている入力を、最後まで打たずに選べるようにするために使います。候補一覧には role="listbox"、各候補には role="option" を付け、開閉状態は入力欄の aria-expanded で示します。

別名

表記ゆれ:サジェスト、入力補完、予測変換、autocomplete

「サジェスト」は検索エンジンの入力補完機能を指すときによく使われる呼び方で、意味としてはオートコンプリートとほぼ同じです。

AI への伝え方

「入力に応じて候補一覧を下に出すオートコンプリートを実装して。候補は配列で持たせて、クリックか矢印キー+Enter で確定できるようにして」のように伝えます。候補の取得元をあらかじめ用意した配列にするか、API から取ってくるかで実装が大きく変わるため、どちらか明示すると狙った動きになります。

似たパーツとの違い

「検索ボックス」は検索を実行するための入力欄そのもので、候補一覧を出す機能は含みません。オートコンプリートは検索ボックスに候補一覧を追加した機能という位置付けです。 「セレクトボックス」はあらかじめ決まった選択肢の中からしか選べませんが、オートコンプリートは文字を打って候補を絞り込める点が異なります。

デモ

コード

HTML
<div class="autocomplete-demo">
  <label class="autocomplete-demo__label" for="autocomplete-demo-input">都道府県</label>
  <div class="autocomplete-demo__combo">
    <input
      class="autocomplete-demo__input"
      type="text"
      id="autocomplete-demo-input"
      role="combobox"
      aria-expanded="false"
      aria-controls="autocomplete-demo-listbox"
      aria-autocomplete="list"
      autocomplete="off"
      placeholder="都道府県名を入力"
    />
    <ul class="autocomplete-demo__listbox" id="autocomplete-demo-listbox" role="listbox" hidden></ul>
  </div>
</div>
CSS
.autocomplete-demo {
  font-family: sans-serif;
  max-width: 280px;
}

.autocomplete-demo__label {
  display: block;
  margin-bottom: 6px;
  font-size: 13px;
  font-weight: 700;
  color: #333;
}

.autocomplete-demo__combo {
  position: relative;
}

.autocomplete-demo__input {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid #b3b3b3;
  border-radius: 8px;
  font-size: 14px;
  color: #333;
  box-sizing: border-box;
}

.autocomplete-demo__input:hover {
  border-color: #666;
}

.autocomplete-demo__input:focus-visible {
  outline: 2px solid #0017c1;
  outline-offset: 2px;
}

.autocomplete-demo__listbox {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  margin: 0;
  padding: 6px;
  list-style: none;
  background: #fff;
  border: 1px solid #ddd;
  border-radius: 8px;
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.12);
  display: block;
  z-index: 10;
}

/* hidden 属性の既定スタイル(display: none)は、上の display: block より
   詳細度が同じため後勝ちで上書きされてしまう。hidden のときは明示的に隠す */
.autocomplete-demo__listbox[hidden] {
  display: none;
}

.autocomplete-demo__option {
  padding: 8px 10px;
  border-radius: 6px;
  font-size: 14px;
  color: #212121;
  cursor: pointer;
}

.autocomplete-demo__option--active {
  background: #0017c1;
  color: #fff;
}
JS
(() => {
  const candidates = ["北海道", "青森県", "岩手県", "宮城県", "秋田県", "山形県", "福島県", "東京都", "神奈川県", "大阪府"];
  const input = document.getElementById("autocomplete-demo-input");
  const listbox = document.getElementById("autocomplete-demo-listbox");
  let items = [];
  let activeIndex = -1;

  function close() {
    listbox.hidden = true;
    listbox.innerHTML = "";
    input.setAttribute("aria-expanded", "false");
    input.removeAttribute("aria-activedescendant");
    items = [];
    activeIndex = -1;
  }

  function highlight(index) {
    const options = listbox.querySelectorAll(".autocomplete-demo__option");
    options.forEach((option, i) => {
      option.classList.toggle("autocomplete-demo__option--active", i === index);
      option.setAttribute("aria-selected", String(i === index));
    });
    activeIndex = index;
    if (index >= 0) {
      input.setAttribute("aria-activedescendant", `autocomplete-demo-option-${index}`);
    } else {
      input.removeAttribute("aria-activedescendant");
    }
  }

  function select(index) {
    input.value = items[index];
    close();
  }

  function open(matches) {
    listbox.innerHTML = "";
    items = matches;
    matches.forEach((text, index) => {
      const li = document.createElement("li");
      li.textContent = text;
      li.id = `autocomplete-demo-option-${index}`;
      li.setAttribute("role", "option");
      li.setAttribute("aria-selected", "false");
      li.className = "autocomplete-demo__option";
      li.addEventListener("mousedown", (event) => {
        event.preventDefault();
        select(index);
      });
      listbox.appendChild(li);
    });
    listbox.hidden = matches.length === 0;
    input.setAttribute("aria-expanded", String(matches.length > 0));
  }

  input.addEventListener("input", () => {
    const value = input.value.trim();
    if (!value) {
      close();
      return;
    }
    open(candidates.filter((c) => c.includes(value)));
    highlight(-1);
  });

  input.addEventListener("keydown", (event) => {
    if (listbox.hidden) return;
    if (event.key === "ArrowDown") {
      event.preventDefault();
      highlight(Math.min(activeIndex + 1, items.length - 1));
    } else if (event.key === "ArrowUp") {
      event.preventDefault();
      highlight(Math.max(activeIndex - 1, 0));
    } else if (event.key === "Enter") {
      if (activeIndex >= 0) {
        event.preventDefault();
        select(activeIndex);
      }
    } else if (event.key === "Escape") {
      close();
    }
  });

  document.addEventListener("click", (event) => {
    if (!event.target.closest(".autocomplete-demo__combo")) {
      close();
    }
  });
})();