コンテンツにスキップ

ライブ検索

ライブ検索は、htmx に配線された検索入力です。ユーザーの入力に応じて 結果領域が更新されます。フォームは JavaScript なしでも動きます — htmx 属性は本物の <form action="/items" method="get"> にぶら下がって いるだけです。

別名: インクリメンタル検索、タイプアヘッド、逐次検索。

下の入力欄にタイプしてみてください — 各キーストローク(300ms の デバウンス付き)が、この docs サイトの Worker が提供するデモ エンドポイントへ実際の htmx リクエストを発行します。エンドポイントは サーバー応答契約をそのまま実装したもので、api/recipes/live-search/ 配下に置かれています(実際のアプリでは /items など自前の URL を 使います)。

<form action="/items" method="get" role="search">
<input
class="hc-input"
type="search"
name="q"
placeholder="Search"
aria-label="Search"
data-hx-get="/items"
data-hx-trigger="input changed delay:300ms, search"
data-hx-target="#results"
data-hx-swap="innerHTML"
data-hx-sync="closest form:replace">
<button class="hc-button" type="submit">Search</button>
</form>
<div id="results" aria-live="polite"></div>
属性効果
data-hx-get="/items"検索エンドポイントに GET。
data-hx-trigger="input changed delay:300ms, search"入力が止まってから 300ms 後、またはネイティブの search イベントで即時発火。
data-hx-target="#results"結果コンテナを差し替える。
data-hx-swap="innerHTML"ターゲットの内側の HTML をスワップ。
data-hx-sync="closest form:replace"実行中のリクエストがあるうちに新しいリクエストが始まったら、古いほうをキャンセル。

フォーム自体は妥当なまま保たれます:

  • <form action="/items" method="get"> は JavaScript なしで動きます — 送信ボタンは同じ q パラメータを同じ URL に送ります。
  • type="search" はネイティブのクリアボタンと、htmx が待ち受ける search イベントを提供します。
  • role="search" はフォームを検索ランドマークとして支援技術に公開します。

@hypermedia-components/core/macros をインポートすれば、同じフォームを 単一のカスタム要素として書けます:

<hc-live-search
action="/items"
target="#results"
name="q"
placeholder="Search items"
label="Search items"
delay="200ms"
submit-label="Search">
</hc-live-search>
<div id="results" aria-live="polite"></div>

<hc-live-search>connectedCallback で自身の子を展開後のフォームに 置き換えます。マクロは結果コンテナを作りません — 検索フォームだけを 出力するので、結果はページの好きな場所に置けます。

属性:

属性デフォルト備考
action(必須)検索エンドポイント。<form action>data-hx-get の両方になります。
target(必須)結果コンテナを指す data-hx-target セレクタ。
nameq入力のクエリパラメータ名。
placeholderSearch入力のプレースホルダー。
label(省略)見える <label> のテキスト。省略時は入力に aria-label が付きます。
aria-labelSearch見えるラベルがないときのフォールバック aria-label。
delay300mshtmx のデバウンス遅延(data-hx-trigger に代入されます)。
submit-labelSearch送信ボタンのテキスト。
swapinnerHTMLdata-hx-swap のモード。
no-submit(真偽値)送信ボタンを省略。

マクロは任意です — これらの属性を超えるカスタマイズは、展開後の HTML を コピーして直接編集してください。

サーバは #results の内側の HTML だけを返すべきです — 外側の <div> は含めません。

<!-- /items?q=foo -->
<ul>
<li><a href="/items/1">Item one</a></li>
<li><a href="/items/2">Item two</a></li>
</ul>

空の状態では、検索が実行されたことがユーザーに伝わるよう、明示的な 空状態マークアップを返します:

<!-- /items?q=zzz -->
<p class="hc-field__message">No results for "zzz".</p>

htmx 以外のリクエスト(JavaScript なしでフォーム送信されたあとのフルページ ロード)が来たときも同じ HTML を返します。サーバは HX-Request: true ヘッダーで htmx リクエストを検知し、レスポンスをフルページレイアウトで 包むかフラグメントだけ送るかを決められます。

リクエストレスポンス
GET /items?q=foo(htmx)200 + #results の内側の HTML
GET /items?q=zzz(該当なし)200 + 明示的な空状態マークアップ
GET /items?q=foo(HX-Request なし — 非 JS 送信)200 + 完全な結果ページ
GET /items?q=foo(失敗)非 2xx — スワップされず、直前の結果が残ります

htmx ≥ 2 はデフォルトで非 2xx レスポンスをスワップしないため、サーバ エラー時は直前の結果がそのまま残ります — フラグメントで置き換えるべき ときにだけ 2xx を返してください。ユーザーに異常を知らせるには、 エラーレスポンスに HX-Trigger: {"hc:toast":{…}} ヘッダーを付けます (トーストレシピを参照)。

検索の実行中にスピナーを表示するには、スピナーを追加して data-hx-indicator でそれを指します:

<div id="results" aria-live="polite"></div>
<!-- Shown by htmx while the input's request is in flight. -->
<span class="hc-spinner htmx-indicator" id="search-spinner" aria-hidden="true"></span>

入力に data-hx-indicator="#search-spinner" を追加してください。

  • <form>role="search" が検索ランドマークを公開します。
  • #resultsaria-live="polite" が、ユーザーのタイピングを遮らずに 新しい結果(または空状態)を読み上げます。
  • ネイティブの type="search" 入力が正しいプリミティブです — クリア ボタン、IME フレンドリーな挙動、Enter / フォーム送信時の search イベントが手に入ります。
  • 結果リストへフォーカスを自動で移さないでください。キーボードユーザーは 入力に留まるべきです。結果へのキーボードナビゲーションを提供する場合は、 キーを明確にドキュメント化してください。

プログレッシブエンハンスメント

Section titled “プログレッシブエンハンスメント”
  • JavaScript なしでは、フォームは action="/items" に送信され、サーバが 完全な結果ページを描画します。
  • htmx ありでは、同じ送信エンドポイントが結果フラグメントを返します。
  • JavaScript はあるが htmx が未読み込みの場合(遅延読み込みなど)でも、 フォームは Enter で普通に送信されます。

要点は、htmx 属性がフォームの基本を決して置き換えないことです。 actionmethod はそのまま残り、htmx は増強するだけです。