検索フォームに文字を入力するとリアルタイムで候補が表示される「サジェスト(オートコンプリート)」は、ユーザーの入力を補助し検索体験を向上させます。この記事では静的データの基本実装・キーボード操作・Ajax連携・IME変換対応・候補内キーワードハイライト・ARIAアクセシビリティまで解説します。
この記事でわかること
- 配列データを使ったサジェストの基本実装(前方一致・部分一致)
- 矢印キー・Enter・Escape でのキーボード操作
- Ajax API からリアルタイムで候補を取得する(debounce付き)
- 日本語IME変換中にサジェストが誤発火しないよう制御する
- 候補リスト内でキーワードをハイライト表示する
- ARIA 属性によるアクセシビリティ対応(スクリーンリーダー)
実装方法の比較
| 方法 | データソース | リアルタイム | カスタマイズ性 | 推奨場面 |
|---|---|---|---|---|
| 自作(配列) | 静的配列 | ○(フロントのみ) | ◎ | 候補が少ない・固定・jQuery既導入 |
| 自作(Ajax) | APIサーバー | ◎ | ◎ | 候補が多い・動的・WordPressなど |
| jQueryUI Autocomplete | 配列・URL | ○ | △ | 手軽に動かしたい(250KB大きい) |
| datalist 要素 | 静的HTML | × | ×(スタイル不可) | 最小限の補完・デザイン不要 |
基本実装:配列データからサジェストを表示する
<div class="suggest-wrapper">
<input type="text" id="search-input"
placeholder="都道府県を入力..."
autocomplete="off"
role="combobox"
aria-autocomplete="list"
aria-controls="suggest-list"
aria-expanded="false">
<ul id="suggest-list" role="listbox" aria-label="検索候補"></ul>
</div>
$(function () {
var prefectures = [
"北海道", "青森県", "岩手県", "宮城県", "秋田県", "山形県", "福島県",
"東京都", "神奈川県", "埼玉県", "千葉県", "茨城県", "栃木県", "群馬県",
"大阪府", "京都府", "兵庫県", "奈良県", "和歌山県", "滋賀県"
];
var $input = $("#search-input");
var $list = $("#suggest-list");
function escapeHtml(str) {
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
function renderList(keyword) {
$list.empty();
if (!keyword) { closeSuggest(); return; }
// 部分一致で絞り込む
var matched = prefectures.filter(function (item) {
return item.includes(keyword);
});
if (!matched.length) { closeSuggest(); return; }
matched.slice(0, 8).forEach(function (item, i) {
// キーワード部分を <strong> でハイライト(XSS対策: escapeHtml で先にエスケープ)
var safeItem = escapeHtml(item);
var safeKw = escapeHtml(keyword);
var html = safeItem.replace(
new RegExp("(" + safeKw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "gi"),
"<strong>$1</strong>"
);
$list.append(
$("<li>").html(html)
.addClass("suggest-item")
.attr({ role: "option", "data-value": item, "data-index": i })
);
});
$list.show();
$input.attr("aria-expanded", "true");
}
function closeSuggest() {
$list.hide();
$input.attr("aria-expanded", "false");
selectedIndex = -1;
}
// 候補クリックで選択
$(document).on("mousedown", ".suggest-item", function (e) {
e.preventDefault(); // blur より先に発火させる
$input.val($(this).data("value"));
closeSuggest();
});
// フォーカス外れたら閉じる
$input.on("blur", function () {
closeSuggest();
});
// 外側クリックでも閉じる
$(document).on("click", function (e) {
if (!$(e.target).closest(".suggest-wrapper").length) {
closeSuggest();
}
});
// input イベント(IME対応は後述)
var selectedIndex = -1;
$input.on("input", function () {
renderList($(this).val().trim());
selectedIndex = -1;
});
});
autocomplete=”off” と mousedown + preventDefault の2点が重要
autocomplete="off" を忘れるとブラウザ標準のサジェストと競合します。また候補クリック時に click ではなく mousedown + preventDefault() を使うと、input の blur より先に処理が走り、候補が消える前に値をセットできます。キーボード操作(矢印キー・Enter・Escape)
// 上記の renderList / closeSuggest 関数が定義済みの前提
$(function () {
var selectedIndex = -1;
$("#search-input").on("keydown", function (e) {
var $items = $(".suggest-item");
var total = $items.length;
if (e.key === "ArrowDown") {
e.preventDefault();
if (!total) return;
selectedIndex = (selectedIndex + 1) % total;
$items.removeClass("is-selected").eq(selectedIndex).addClass("is-selected");
// aria-activedescendant で現在選択中の候補を通知
$(this).attr("aria-activedescendant", $items.eq(selectedIndex).attr("id"));
} else if (e.key === "ArrowUp") {
e.preventDefault();
if (!total) return;
selectedIndex = (selectedIndex - 1 + total) % total;
$items.removeClass("is-selected").eq(selectedIndex).addClass("is-selected");
$(this).attr("aria-activedescendant", $items.eq(selectedIndex).attr("id"));
} else if (e.key === "Enter") {
if (selectedIndex >= 0 && total) {
e.preventDefault();
$(this).val($items.eq(selectedIndex).data("value"));
closeSuggest();
}
// selectedIndex が -1 ならデフォルトのフォーム送信を通す
} else if (e.key === "Escape") {
closeSuggest();
}
});
});
Enter キーはフォーム送信と競合に注意
selectedIndex === -1(候補が選択されていない)のときは preventDefault() を呼ばないことで、通常のフォーム送信(Enterキーでの検索実行)が妨げられません。サジェストから候補を選んでいる場合のみ送信を抑制し、候補を確定させます。日本語IME変換中の誤発火を防ぐ
日本語の変換中(ひらがな入力中)にも input イベントが発火するため、変換確定前にサジェストが表示・更新される問題が起きます。compositionstart / compositionend イベントで制御します。
$(function () {
var isComposing = false; // IME変換中フラグ
$("#search-input")
.on("compositionstart", function () {
isComposing = true; // 変換開始(ひらがな入力中)
})
.on("compositionend", function () {
isComposing = false; // 変換確定
// 確定時点でサジェストを更新
renderList($(this).val().trim());
})
.on("input", function () {
if (isComposing) return; // 変換中はスキップ
renderList($(this).val().trim());
});
});
Chrome と Safari で compositionend のタイミングが異なる
Chromeでは
Chromeでは
compositionend の後に input が発火します。Safariでは順番が逆の場合があります。compositionend ハンドラー内で直接 renderList() を呼ぶことでどちらのブラウザでも確実に変換確定後に処理できます。Ajax API からリアルタイムで候補を取得する
候補データが多い場合や頻繁に更新される場合は、Ajaxでサーバーから取得します。debounce でAPIリクエストの頻度を抑えることが重要です。
$(function () {
var debounceTimer;
var lastXhr; // 前のリクエストをキャンセルするため
function escapeHtml(str) {
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
$("#search-input").on("input", function () {
var keyword = $(this).val().trim();
clearTimeout(debounceTimer);
if (!keyword) {
$("#suggest-list").empty().hide();
return;
}
// 300ms 入力がなければ API リクエスト
debounceTimer = setTimeout(function () {
// 前のリクエストが進行中なら中断
if (lastXhr) lastXhr.abort();
lastXhr = $.ajax({
url: "/api/suggest",
data: { q: keyword },
success: function (data) {
var $list = $("#suggest-list").empty();
if (!data.length) { $list.hide(); return; }
data.slice(0, 8).forEach(function (item) {
// 候補テキスト内のキーワードをハイライト
var safeItem = escapeHtml(item);
var safeKw = escapeHtml(keyword);
var highlighted = safeItem.replace(
new RegExp("(" + safeKw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "gi"),
"<strong>$1</strong>"
);
$list.append($("<li>").html(highlighted).addClass("suggest-item").data("value", item));
});
$list.show();
},
error: function (xhr) {
if (xhr.statusText !== "abort") {
$("#suggest-list").empty().hide();
}
}
});
}, 300);
});
});
WordPressで Ajax サジェストを実装する場合
WordPressでAjax APIを作る場合は
WordPressでAjax APIを作る場合は
admin-ajax.php と wp_ajax_ フックを使います。AjaxのURLは管理画面では ajaxurl、フロントエンドでは wp_localize_script() で渡します。Ajax通信全般は$.post()完全ガイドも参照してください。ARIA 属性によるアクセシビリティ対応
サジェストUIはスクリーンリーダーにとって複雑な操作を伴います。ARIA の combobox パターンを使って候補の変化を通知します。
<div class="suggest-wrapper">
<label for="search-input">検索</label>
<input type="text" id="search-input"
autocomplete="off"
role="combobox"
aria-autocomplete="list"
aria-controls="suggest-list"
aria-expanded="false"
aria-haspopup="listbox">
<ul id="suggest-list"
role="listbox"
aria-label="検索候補">
<!-- 動的に生成 -->
<!-- <li role="option" id="suggest-0">東京都</li> -->
</ul>
</div>
// 候補リストを更新するたびに aria-expanded と aria-activedescendant を管理
function renderList(keyword) {
var $input = $("#search-input");
var $list = $("#suggest-list").empty();
if (!keyword) {
$list.hide();
$input.attr("aria-expanded", "false").removeAttr("aria-activedescendant");
return;
}
var matched = data.filter(function (item) { return item.includes(keyword); });
matched.slice(0, 8).forEach(function (item, i) {
var id = "suggest-" + i;
$list.append(
$("<li>").text(item)
.addClass("suggest-item")
.attr({ id: id, role: "option", "data-value": item })
);
});
if (matched.length) {
$list.show();
$input.attr("aria-expanded", "true");
} else {
$list.hide();
$input.attr("aria-expanded", "false");
}
}
ARIA combobox パターンの必須属性
role="combobox": 入力欄がサジェスト付きであることを示すaria-expanded: サジェストが開いているか("true"/"false")aria-controls: 対応するリスト要素のIDaria-activedescendant: 現在フォーカス中の候補のIDrole="listbox"・role="option": リスト・候補の役割
サジェストボックスのCSSスタイル
.suggest-wrapper {
position: relative;
display: inline-block;
width: 100%;
max-width: 400px;
}
#suggest-list {
display: none;
position: absolute;
top: 100%;
left: 0;
width: 100%;
max-height: 240px;
overflow-y: auto;
margin: 0;
padding: 0;
list-style: none;
background: #fff;
border: 1px solid #d1d5db;
border-top: none;
border-radius: 0 0 6px 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 100;
}
.suggest-item {
padding: 8px 12px;
cursor: pointer;
font-size: 0.95rem;
}
.suggest-item:hover,
.suggest-item.is-selected {
background-color: #eff6ff;
color: #1d4ed8;
}
/* 候補内のハイライト(入力済みキーワード) */
.suggest-item strong {
font-weight: bold;
color: #1d4ed8;
}
まとめ
jQueryで検索サジェストを実装する方法を用途別に選んでください。
- 候補が少ない: 配列 +
includes()の部分一致フィルター - 候補が多い・動的: Ajax + debounce(300ms)+ 前リクエスト中断
- 日本語対応:
compositionstart/endで IME 変換中をスキップ - 候補ハイライト:
escapeHtml()でXSS対策後に<strong>で囲む - キーボード: ArrowDown/Up でインデックス管理・Enter 確定・Escape で閉じる
- アクセシビリティ:
role="combobox"・aria-expanded・aria-activedescendant - 候補クリック:
mousedown+preventDefault()で blur より先に処理
関連記事: リストをリアルタイムに絞り込む完全ガイド / $.post()でPOSTリクエストを送信する完全ガイド / フォームバリデーション完全ガイド
よくある質問(FAQ)
Qサジェストが開いたままになります。外をクリックしたら閉じたいです。
A
$(document).on("click", function(e) { if (!$(e.target).closest(".suggest-wrapper").length) { closeSuggest(); } });でdocumentのクリックを監視し、サジェストの外側がクリックされたら非表示にします。また $input.on("blur", closeSuggest) も合わせると、フォーカスが外れた時も閉じます。候補クリックとblurが競合する場合は mousedown + preventDefault で対処してください。Q選択した候補でそのままフォームを送信したいです。
A
$input.val(selectedValue).closest("form").submit(); のように、値をセット後すぐに submit() を呼んでください。Enterキーによる選択時も同様に処理できます。WordPressの検索フォームでは action に /?s= が設定されているため、val() で値をセットしてから submit() で通常の検索が実行されます。Q最近の検索履歴をサジェストに表示したいです。
A
localStorage に検索履歴を保存して候補として表示できます。検索実行時に localStorage.getItem("history") で取得した配列に追加し、入力フォーカス時に keyword が空の場合は履歴を表示、入力がある場合は通常の候補と履歴を組み合わせて表示する実装が一般的です。Qモバイルでタッチ操作がうまくいきません。
Aモバイルでは
click イベントに遅延が発生し、候補が消えた後に発火することがあります。mousedown + preventDefault() を使うと blur より先に処理が走るため解決します:$(document).on("mousedown", ".suggest-item", function(e) { e.preventDefault(); ... });タッチデバイスでは touchstart イベントも同様に使えます。QjQueryUIを使わず軽量なライブラリはありますか?
AjQueryUIは全体で約250KBと大きいため、サジェストのみの目的には重すぎます。軽量な代替として autocomplete.js(約8KB)や autoComplete.js(約6KB)があります。ただし機能の組み合わせが決まっている場合は、この記事の自作実装の方がカスタマイズしやすい場合があります。