htmx 逆引きレシピ
入力しながら検索するには?

公開日:
最終更新日:

htmxのライブ検索は、入力や条件変更に合わせてサーバーへ問い合わせ、結果部分だけをその場で更新できる仕組みです。
このページでは、業務UIでよくある「顧客/商品検索」「申請一覧の絞り込み」「社員番号フィルタ」を例に、よく使うhtmx属性と実装のコツをデモ付きでまとめます。

使うのは主に hx-get / hx-trigger / hx-target / hx-params / hx-sync
delayで負荷を抑える送るパラメータを絞る古い検索結果で上書きされない――といった現場の落とし穴も押さえながら、コピペで実践投入できる形にしています。

使用するhtmx属性

  • hx-get:入力や条件変更のたびに、GETで検索結果HTMLを取得して差し替える(ライブ検索向き)
  • hx-trigger:リクエストを発火するタイミングを指定(例:keyup changed delay:300ms / change / search
  • hx-target:返ってきたHTMLを差し替える先の要素(CSSセレクタ)を指定
  • hx-params:送信するパラメータを絞る(例:qだけ/q,statusだけ)
  • hx-sync:リクエストが重なった時の挙動を制御し、古い結果で上書きされる事故を防ぐ(例:this:replace

利用シーン

  • 「顧客/商品検索」:入力しながら候補を絞り込み、結果エリアだけ即更新したい
  • 「申請一覧の条件検索」:キーワード+ステータスなど複数条件で一覧をその場で更新したい
  • 「社員番号で絞り込み」:社員番号(数字)入力で対象者だけを即表示したい

① 顧客/商品検索(ライブ検索)

入力しながら、顧客名や商品名の候補をサッと絞り込める「ライブ検索」の例です。
ページ遷移なしで結果だけが更新されるので、検索UIをシンプルに気持ちよく作れます。

HTML

<div class="DEMO">

  <h4>① 顧客/商品検索(ライブ検索)</h4>

  <label class="FORM-LABEL">
    キーワード(名前 / 商品名)
    <input
      type="text"
      name="q"
      class="BD2"
      placeholder="例)田中 / みかん / PC…"
      autocomplete="off"

      hx-get="/htmx/demo/_live_search_1.php"
      hx-trigger="keyup changed delay:300ms, search"
      hx-target="#DEMO_LIVE_1_RESULT"
      hx-params="q"
      hx-sync="this:replace"
    >
  </label>

  <div id="DEMO_LIVE_1_RESULT" class="RESULT" aria-live="polite">
    <div class="HTMX-NOTE">↑ 入力するとここに検索結果が出ます</div>
  </div>

</div>

PHP

<?php
// 厳格モード(型の扱いを厳しくする)
declare(strict_types=1);

// 返す内容はHTMLだよ(文字コードはUTF-8)
header('Content-Type: text/html; charset=UTF-8');

// GETパラメータ q を受け取る(なければ空文字)
$q = (string)($_GET['q'] ?? '');

// 前後の空白を除去する
$q = trim($q);

// デモ用:顧客/商品を混ぜたリスト(本来はDB検索など)
$items = [
  ['type' => '顧客', 'name' => '田中 太郎', 'meta' => '東京'],
  ['type' => '顧客', 'name' => '田中 花子', 'meta' => '大阪'],
  ['type' => '顧客', 'name' => '佐藤 次郎', 'meta' => '福岡'],
  ['type' => '商品', 'name' => 'みかんジュース', 'meta' => '飲料'],
  ['type' => '商品', 'name' => 'みかんゼリー', 'meta' => 'スイーツ'],
  ['type' => '商品', 'name' => 'PCスタンド', 'meta' => '周辺機器'],
];

// (デモ)検索っぽさを出すため、少しだけ待つ
usleep(150000);

// もし入力が空なら、案内を表示して終了
if ($q === '') {
  // HTMLを出力
  echo '<div class="HTMX-NOTE">キーワードを入力してください(例:田中 / みかん)</div>';

  // ここで処理を終わる
  exit;
}

// 大文字小文字の差をなくして検索するため、入力を小文字化
$qq = mb_strtolower($q);

// 条件に合うものだけを抽出(部分一致)
$hits = array_values(array_filter($items, function ($it) use ($qq) {
  // name を小文字化
  $name = mb_strtolower((string)$it['name']);

  // name の中に入力文字が含まれているか(含まれていれば true)
  return mb_strpos($name, $qq) !== false;
}));

// 見出し(検索語と件数)を表示
echo '<div class="RESULT-HEAD">';

// 「検索:」のラベル部分
echo '<strong>検索:</strong>';

// ユーザー入力をそのまま出すと危険なので htmlspecialchars でエスケープして出力
echo htmlspecialchars($q, ENT_QUOTES, 'UTF-8');

// 件数を表示
echo '(' . count($hits) . '件)';

// 見出しを閉じる
echo '</div>';

// 0件なら「該当なし」を表示して終了
if (!$hits) {
  // 0件のメッセージ
  echo '<div class="FORM-RESULT is-ng"><strong>該当なし</strong></div>';

  // 終了
  exit;
}

// 結果をリストで表示開始
echo '<ul class="RESULT-LIST">';

// 10件まで表示(デモ用)
foreach (array_slice($hits, 0, 10) as $it) {

  // type を安全に表示するためエスケープ
  $type = htmlspecialchars((string)$it['type'], ENT_QUOTES, 'UTF-8');

  // name を安全に表示するためエスケープ
  $name = htmlspecialchars((string)$it['name'], ENT_QUOTES, 'UTF-8');

  // meta を安全に表示するためエスケープ
  $meta = htmlspecialchars((string)$it['meta'], ENT_QUOTES, 'UTF-8');

  // 1件分のHTMLを出力
  echo "<li><span class=\"TAG\">{$type}</span> {$name} <small>{$meta}</small></li>";
}

// リストを閉じる
echo '</ul>';

デモ

① 顧客/商品検索(ライブ検索)

↑ 入力するとここに検索結果が出ます

解説

  • 検索欄に文字を打つたびに、htmxがサーバーへGETリクエストを送ります(hx-get)。
  • いつ送るかはhx-triggerで指定し、入力後に少し待ってから(delay)検索します(連打による負荷を軽減)。
  • サーバー(PHP)は受け取ったqで検索し、結果のHTMLだけを返します。
  • 返ってきたHTMLはhx-targetで指定した結果エリアにだけ差し替えられます(ページ全体は更新しません)。
  • hx-params="q"で、送るのは検索語qだけに絞れます(余計な値を送らない)。
  • hx-sync="this:replace"で、古い検索結果が後から返ってきて上書きする事故を防ぎ、常に最新入力の結果を表示します。

② 申請一覧の検索(条件つきライブ検索)

キーワードやステータスを変えるたびに、申請一覧をその場で更新できる「条件つきライブ検索」の例です。
一覧画面を再読み込みせずに絞り込みができるので、管理画面の検索UIにそのまま使えます。

HTML

<div class="DEMO">

  <h4>② 申請一覧の検索(条件つきライブ検索)</h4>

  <form
    id="DEMO_LIVE_2_FORM"
    class="FORM"
    hx-get="/htmx/demo/_live_search_2.php"
    hx-trigger="change, keyup changed delay:300ms from:input[name='q'], search from:input[name='q']"
    hx-target="#DEMO_LIVE_2_RESULT"
    hx-params="q,status"
    hx-sync="this:replace"
  >
    <label>
      キーワード
      <input type="text" name="q" placeholder="例)稟議 / 出張 / 佐藤…" autocomplete="off">
    </label>

    <label>
      ステータス
      <select name="status">
        <option value="">すべて</option>
        <option value="draft">下書き</option>
        <option value="review">承認待ち</option>
        <option value="approved">承認済み</option>
        <option value="rejected">差戻し</option>
      </select>
    </label>
  </form>

  <div id="DEMO_LIVE_2_RESULT" class="RESULT" aria-live="polite">
    <div class="HTMX-NOTE">↑ 条件を変えるとここに一覧が更新されます</div>
  </div>

</div>

PHP

<?php
// 厳格モード
declare(strict_types=1);

// HTML(UTF-8)として返す
header('Content-Type: text/html; charset=UTF-8');

// GETパラメータ q を受け取る(なければ空)
$q = (string)($_GET['q'] ?? '');

// 前後空白を除去
$q = trim($q);

// GETパラメータ status を受け取る(なければ空)
$status = (string)($_GET['status'] ?? '');

// 前後空白を除去
$status = trim($status);

// デモ用:申請データ(本来はDB)
$rows = [
  ['id'=>101,'title'=>'出張申請(大阪)','user'=>'佐藤','status'=>'review','date'=>'2026-01-05'],
  ['id'=>102,'title'=>'稟議:PC入替','user'=>'田中','status'=>'approved','date'=>'2025-12-28'],
  ['id'=>103,'title'=>'備品購入(みかん箱)','user'=>'鈴木','status'=>'draft','date'=>'2025-12-20'],
  ['id'=>104,'title'=>'交通費精算','user'=>'佐藤','status'=>'rejected','date'=>'2025-12-18'],
];

// (デモ)少し待つ
usleep(120000);

// 条件に合う行だけ抽出する
$hits = array_values(array_filter($rows, function($r) use ($q, $status) {

  // status が指定されていて、行のstatusと違うなら除外
  if ($status !== '' && $r['status'] !== $status) {
    return false;
  }

  // q が空なら(キーワードなし)status条件だけでOKなので通す
  if ($q === '') {
    return true;
  }

  // キーワードを小文字化
  $qq = mb_strtolower($q);

  // タイトル+申請者名をまとめて検索対象にする
  $hay = mb_strtolower($r['title'] . ' ' . $r['user']);

  // 部分一致で含まれるか
  return mb_strpos($hay, $qq) !== false;
}));

// status表示用(英→日本語)
$labelMap = [
  'draft'    => '下書き',
  'review'   => '承認待ち',
  'approved' => '承認済み',
  'rejected' => '差戻し',
];

// 見出しを表示開始
echo '<div class="RESULT-HEAD">';

// 条件ラベル
echo '<strong>条件:</strong> ';

// q の表示(空なら「なし」)
echo 'q=' . ($q === '' ? '(なし)' : htmlspecialchars($q, ENT_QUOTES, 'UTF-8')) . ' / ';

// status の表示(空なら「すべて」)
echo 'status=' . ($status === ''
  ? '(すべて)'
  : htmlspecialchars($labelMap[$status] ?? $status, ENT_QUOTES, 'UTF-8')
);

// 件数表示
echo ' <strong>' . count($hits) . '件</strong>';

// 見出しを閉じる
echo '</div>';

// 0件ならメッセージ出して終了
if (!$hits) {
  // 0件表示
  echo '<div class="FORM-RESULT is-ng"><strong>該当なし</strong></div>';

  // 終了
  exit;
}

// テーブルの開始
echo '<table class="TABLE">';

// 見出し行
echo '<thead><tr><th>ID</th><th>タイトル</th><th>申請者</th><th>状態</th><th>日付</th></tr></thead>';

// 本体開始
echo '<tbody>';

// 1行ずつ表示
foreach ($hits as $r) {

  // ID(数値なのでint化)
  $id = (int)$r['id'];

  // タイトルを安全に表示するためエスケープ
  $title = htmlspecialchars((string)$r['title'], ENT_QUOTES, 'UTF-8');

  // 申請者名をエスケープ
  $user  = htmlspecialchars((string)$r['user'], ENT_QUOTES, 'UTF-8');

  // statusキーを取り出す
  $stKey = (string)$r['status'];

  // 日本語ラベルに変換してエスケープ
  $st    = htmlspecialchars((string)($labelMap[$stKey] ?? $stKey), ENT_QUOTES, 'UTF-8');

  // 日付をエスケープ
  $date  = htmlspecialchars((string)$r['date'], ENT_QUOTES, 'UTF-8');

  // 1行分のHTMLを出力
  echo "<tr><td>{$id}</td><td>{$title}</td><td>{$user}</td><td>{$st}</td><td>{$date}</td></tr>";
}

// tbodyを閉じる
echo '</tbody>';

// tableを閉じる
echo '</table>';

デモ

② 申請一覧の検索(条件つきライブ検索)

↑ 条件を変えるとここに一覧が更新されます

解説

  • フォーム全体にhx-getを付け、条件が変わるたびにサーバーへGETで検索リクエストを送ります。
  • hx-triggerステータス変更(change)キーワード入力(keyup + delay)の両方を拾い、操作感よく更新します。
  • hx-params="q,status"で、送信するのはキーワードステータスだけに絞ります(不要な値を送らない)。
  • サーバー(PHP)はqstatusで絞り込み、一覧テーブルのHTMLだけを返します。
  • 返ってきたHTMLはhx-targetで指定した結果エリアにだけ差し替えられます(ページ全体は更新しません)。
  • hx-sync="this:replace"でリクエストの競合を整理し、古い結果で上書きされる事故を防いで常に最新条件の一覧を表示します。

③ 社員番号で絞り込み

社員番号を入力するだけで、該当する社員をその場で絞り込める「社員番号フィルタ」の例です。
数字入力に特化したUIなので、名簿検索や勤怠・申請画面の「対象者選択」に向いています。

HTML

<div class="DEMO">

  <h4>③ 社員番号で絞り込み</h4>

  <label class="FORM-LABEL">
    社員番号(数字)
    <input
      type="text"
      name="empno"
      inputmode="numeric"
      pattern="[0-9]*"
      placeholder="例)1024"
      autocomplete="off"

      hx-get="/htmx/demo/_live_search_3.php"
      hx-trigger="keyup changed delay:250ms, search"
      hx-target="#DEMO_LIVE_3_RESULT"
      hx-params="empno"
      hx-sync="this:replace"
    >
  </label>

  <div id="DEMO_LIVE_3_RESULT" class="RESULT" aria-live="polite">
    <div class="HTMX-NOTE">↑ 社員番号を入力すると一致候補を表示します</div>
  </div>

</div>

PHP

<?php
// 厳格モード
declare(strict_types=1);

// HTML(UTF-8)として返す
header('Content-Type: text/html; charset=UTF-8');

// GETパラメータ empno を受け取る(なければ空)
$empno = (string)($_GET['empno'] ?? '');

// 前後空白を除去
$empno = trim($empno);

// デモ用:社員データ(本来はDB)
$emps = [
  ['empno'=>'1001','name'=>'佐藤 次郎','dept'=>'開発'],
  ['empno'=>'1002','name'=>'田中 花子','dept'=>'総務'],
  ['empno'=>'1024','name'=>'鈴木 みかん','dept'=>'営業'],
  ['empno'=>'1100','name'=>'高橋 太郎','dept'=>'開発'],
];

// (デモ)少し待つ
usleep(100000);

// 入力が空ではなく、数字だけではない場合はエラー表示して終了
if ($empno !== '' && !preg_match('/^\d+$/', $empno)) {
  // エラーを表示
  echo '<div class="FORM-RESULT is-ng"><strong>数字のみ</strong>で入力してください。</div>';

  // 終了
  exit;
}

// 入力が空なら案内を出して終了
if ($empno === '') {
  // 案内を表示
  echo '<div class="HTMX-NOTE">社員番号を入力してください(部分一致でもOK)</div>';

  // 終了
  exit;
}

// 部分一致でフィルタ(例:10 で 1001,1002,1024…)
$hits = array_values(array_filter($emps, function($e) use ($empno) {

  // empno の中に入力文字が含まれているか
  return strpos($e['empno'], $empno) !== false;
}));

// 見出しを表示
echo '<div class="RESULT-HEAD">';

// ラベルを表示
echo '<strong>社員番号:</strong>';

// 入力を安全に表示
echo htmlspecialchars($empno, ENT_QUOTES, 'UTF-8');

// 件数を表示
echo '(' . count($hits) . '件)';

// 見出しを閉じる
echo '</div>';

// 0件なら該当なし表示して終了
if (!$hits) {
  // 0件表示
  echo '<div class="FORM-RESULT is-ng"><strong>該当なし</strong></div>';

  // 終了
  exit;
}

// リスト開始
echo '<ul class="RESULT-LIST">';

// 1件ずつ表示
foreach ($hits as $e) {

  // 社員番号をエスケープ
  $no   = htmlspecialchars((string)$e['empno'], ENT_QUOTES, 'UTF-8');

  // 氏名をエスケープ
  $name = htmlspecialchars((string)$e['name'], ENT_QUOTES, 'UTF-8');

  // 部署をエスケープ
  $dept = htmlspecialchars((string)$e['dept'], ENT_QUOTES, 'UTF-8');

  // 1件分のHTMLを出力
  echo "<li><span class=\"TAG\">{$no}</span> {$name} <small>{$dept}</small></li>";
}

// リストを閉じる
echo '</ul>';

デモ

③ 社員番号で絞り込み

↑ 社員番号を入力すると一致候補を表示します

解説

  • 入力欄にhx-getを付け、入力のたびにサーバーへGETで検索リクエストを送ります。
  • hx-trigger入力(keyup)を拾い、delayを入れて連打による負荷を抑えながら検索します。
  • hx-params="empno"で、送信するのは社員番号(empno)だけに限定します。
  • サーバー(PHP)は受け取った社員番号で絞り込み、一致した社員リストのHTMLだけを返します。
  • 返ってきたHTMLはhx-targetで指定した結果エリアに差し替えられます(ページ全体は更新しません)。
  • hx-sync="this:replace"により、古いレスポンスで上書きされる事故を防ぎ、常に最新入力の結果が表示されます。

このページの著者

もちもちみかん(システムエンジニア)

社内SEとしてグループ企業向けの業務アプリを要件定義〜運用まで一気通貫で担当しています。

経験:Webアプリ/業務システム

得意:PHP・JavaScript・MySQL・CSS

個人実績:フォーム生成基盤クイズ学習プラットフォーム

詳しいプロフィールはこちら!  もちもちみかんのプロフィール

もちもちみかん0系くん
TOPへ

もちもちみかん.comとは


このサイトでは、コーディングがめんどうくさい人向けのお助けツールとして、フォームやCSSをノーコードで生成できる、
 もちもちみかん.forms
 もちもちみかん.css1
 もちもちみかん.css2
と言ったジェネレーターを用意してます。

また、このサイトを通じて、「もちもちみかん」のかわいさを普及したいとかんがえてます!