htmx 逆引きレシピ
チェック/セレクトで絞り込むには?
公開日:
最終更新日:
チェックボックスやセレクトで条件を切り替え、一覧をその場で絞り込める「フィルタ」パターンをhtmxで実装します。
このページでは「担当者/ステータスで絞る」「期間で絞る」「権限別に表示を切り替える」の3例を通して、結果部分だけを更新する実践形をデモ付きでまとめます。
使うのは主に hx-get / hx-trigger / hx-include / hx-params / hx-push-urlです。
使用するhtmx属性
hx-get:条件変更のたびに、GETでフィルタ結果HTMLを取得して差し替える(一覧の絞り込み向き)hx-trigger:リクエストを発火するタイミングを指定(例:load/change from:FORM/reset from:FORM)hx-include:リクエストに含める要素を指定(例:フォーム全体を送り、チェック/セレクトの値をまとめて送信)hx-params:送信するパラメータを絞る(例:assignee,status/from,to,type/role)hx-target:返ってきたHTMLを差し替える先の要素(CSSセレクタ)を指定(例:結果エリア自身に差し替え)hx-push-url:URLや履歴の扱いを制御(このレシピではhx-push-url="false"で履歴が積み上がるのを防止)
利用シーン
- 「担当者/ステータスで絞る」:チェック+セレクトで条件を切り替え、一覧をその場でフィルタしたい
- 「期間で絞る」:開始日/終了日の指定で、対象期間のデータだけを一覧に反映したい
- 「権限別に表示を切り替える」:閲覧者/管理者などの権限に応じて、見せる列や情報量を切り替えたい
① 担当者/ステータスで絞る
担当者(セレクト)とステータス(チェック)を切り替えて、一覧をその場で絞り込むフィルタの例です。
ページ全体は再読み込みせず、結果エリアだけを更新するので、管理画面の一覧検索にそのまま使えます。
HTML
<div class="DEMO">
<h4>① 担当者/ステータスで絞る</h4>
<form id="DEMO_FILTER_1_FORM" class="FORM">
<label>
担当者
<select name="assignee">
<option value="">全員</option>
<option value="tanaka">田中</option>
<option value="sato">佐藤</option>
<option value="suzuki">鈴木</option>
</select>
</label>
<fieldset class="FORM-FIELDSET">
<legend>ステータス(複数選択OK)</legend>
<label><input type="checkbox" name="status" value="draft"> 下書き</label>
<label><input type="checkbox" name="status" value="review"> 承認待ち</label>
<label><input type="checkbox" name="status" value="approved"> 承認済み</label>
<label><input type="checkbox" name="status" value="rejected"> 差戻し</label>
</fieldset>
<button type="reset" class="BTN is-sub">条件クリア</button>
</form>
<div
id="DEMO_FILTER_1_RESULT"
class="RESULT"
aria-live="polite"
hx-get="/htmx/demo/_filter_1.php"
hx-trigger="load, change from:#DEMO_FILTER_1_FORM, reset from:#DEMO_FILTER_1_FORM delay:10ms"
hx-include="#DEMO_FILTER_1_FORM"
hx-params="assignee,status"
hx-push-url="false"
hx-target="this"
>
</div>
</div>
PHP
<?php
// 型を厳密に扱う(思わぬ型変換を減らす)
declare(strict_types=1);
// 返すのはHTML(UTF-8)
header('Content-Type: text/html; charset=UTF-8');
// HTMLに安全に出すための関数(XSS対策)
function h(string $s): string {
// HTML特殊文字をエスケープする
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
// GETパラメータ assignee を受け取る(なければ空文字)
$assignee = (string)($_GET['assignee'] ?? '');
// 前後の空白を削る
$assignee = trim($assignee);
// GETパラメータ status を受け取る(チェックボックスは複数来るので注意)
$rawStatus = $_GET['status'] ?? [];
// status が配列でない場合(1個だけ来た等)は配列に統一する
if (!is_array($rawStatus)) $rawStatus = [$rawStatus];
// 担当者のホワイトリスト(許可された値だけ受け付ける)
$allowedAssignee = ['tanaka' => '田中', 'sato' => '佐藤', 'suzuki' => '鈴木'];
// ステータスのホワイトリスト(許可された値だけ受け付ける)
$allowedStatus = ['draft', 'review', 'approved', 'rejected'];
// 担当者がホワイトリストにある場合だけ採用、なければ空(全員扱い)
$assignee = array_key_exists($assignee, $allowedAssignee) ? $assignee : '';
// status 用の配列(検証済みだけ入れる)
$status = [];
// 送られてきた status を1つずつ確認する
foreach ($rawStatus as $v) {
// 値を文字列として扱う
$v = (string)$v;
// 許可された status なら配列へ追加
if (in_array($v, $allowedStatus, true)) $status[] = $v;
}
// 重複を消して、添字も 0,1,2… に詰める
$status = array_values(array_unique($status));
// デモ用の一覧データ(本番はDBなど)
$rows = [
['id'=>101,'title'=>'出張申請(大阪)','assignee'=>'sato','status'=>'review','date'=>'2026-01-05'],
['id'=>102,'title'=>'稟議:PC入替','assignee'=>'tanaka','status'=>'approved','date'=>'2025-12-28'],
['id'=>103,'title'=>'備品購入(みかん箱)','assignee'=>'suzuki','status'=>'draft','date'=>'2025-12-20'],
['id'=>104,'title'=>'交通費精算','assignee'=>'sato','status'=>'rejected','date'=>'2025-12-18'],
['id'=>105,'title'=>'稟議:モニター購入','assignee'=>'tanaka','status'=>'review','date'=>'2025-12-16'],
];
// ステータス表示用(英→日本語)
$labelStatus = [
'draft' => '下書き',
'review' => '承認待ち',
'approved' => '承認済み',
'rejected' => '差戻し',
];
// 条件に合う行だけ残す
$hits = array_values(array_filter($rows, function($r) use ($assignee, $status) {
// 担当者が指定されていて、行の担当と違うなら除外
if ($assignee !== '' && $r['assignee'] !== $assignee) return false;
// ステータスが1つ以上指定されていて、行のステータスが含まれないなら除外
if ($status && !in_array($r['status'], $status, true)) return false;
// ここまで来たら採用
return true;
}));
// 条件の見出し部分を表示
echo '<div class="RESULT-HEAD">';
// 条件ラベル
echo '<strong>条件:</strong> ';
// 担当者の条件表示(空なら全員)
echo '担当者=' . ($assignee === '' ? '(全員)' : h($allowedAssignee[$assignee]));
// 区切り
echo ' / ';
// ステータスの条件表示(空なら指定なし)
echo 'ステータス=' . (
$status
? h(implode(',', array_map(fn($s) => $labelStatus[$s], $status)))
: '(指定なし)'
);
// 件数表示
echo ' <strong>' . count($hits) . '件</strong>';
// 見出し終了
echo '</div>';
// 0件ならメッセージを出して終了
if (!$hits) {
// 該当なし表示
echo '<div class="FORM-RESULT is-ng"><strong>該当なし</strong></div>';
// 終了
exit;
}
// テーブル装飾用div開始
echo '<div class="TABLE_WRAPPER">';
// テーブル開始
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は数値として扱う
$id = (int)$r['id'];
// タイトルは安全に表示
$title = h((string)$r['title']);
// 担当者はラベルに変換して安全に表示
$as = h($allowedAssignee[$r['assignee']] ?? (string)$r['assignee']);
// ステータスはラベルに変換して安全に表示
$st = h($labelStatus[$r['status']] ?? (string)$r['status']);
// 日付は安全に表示
$date = h((string)$r['date']);
// 1行出力
echo "<tr><td>{$id}</td><td>{$title}</td><td>{$as}</td><td>{$st}</td><td>{$date}</td></tr>";
}
// tbody終了
echo '</tbody>';
// table終了
echo '</table>';
// div終了
echo '</div>';
デモ
① 担当者/ステータスで絞る
解説
- フォームの値(担当者・ステータス)が変わるたびに、
hx-getでサーバーへGETリクエストを送ります。 hx-triggerでchange(選択/チェック変更)とreset(条件クリア)を拾い、条件変更のたびに自動で再検索します。hx-includeでフォーム全体をリクエストに含め、担当者と複数のステータス値をまとめて送ります。hx-params="assignee,status"で送信するパラメータを絞り、必要な条件だけをサーバーへ渡します。- サーバー(PHP)は受け取った条件でデータを絞り込み、結果テーブルのHTMLを返します。
- 返ってきたHTMLは
hx-targetで指定した結果エリアに差し替えられ、ページ遷移なしで一覧だけが更新されます。 - このデモでは
hx-push-url="false"にして、フィルタ操作のたびに履歴が積み上がるのを防いでいます。
② 期間で絞る
開始日・終了日を指定して、対象期間のデータだけを一覧に表示するフィルタの例です。
条件を変えるたびに結果エリアだけが更新されるので、日付レンジ検索を手軽に組み込めます。
HTML
<div class="DEMO">
<h4>② 期間で絞る</h4>
<form id="DEMO_FILTER_2_FORM" class="FORM">
<label>
開始日
<input type="date" name="from">
</label>
<label>
終了日
<input type="date" name="to">
</label>
<label>
種類(任意)
<select name="type">
<option value="">すべて</option>
<option value="travel">出張</option>
<option value="expense">精算</option>
<option value="purchase">購買</option>
</select>
</label>
<button type="reset" class="BTN is-sub">条件クリア</button>
</form>
<div
id="DEMO_FILTER_2_RESULT"
class="RESULT"
aria-live="polite"
hx-get="/htmx/demo/_filter_2.php"
hx-trigger="load, change from:#DEMO_FILTER_2_FORM, reset from:#DEMO_FILTER_2_FORM delay:10ms"
hx-include="#DEMO_FILTER_2_FORM"
hx-params="from,to,type"
hx-push-url="false"
hx-target="this"
>
</div>
</div>
PHP
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 返すのはHTML(UTF-8)
header('Content-Type: text/html; charset=UTF-8');
// HTMLに安全に出すための関数
function h(string $s): string {
// HTML特殊文字をエスケープする
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
// GETパラメータ from(開始日)を受け取る
$from = (string)($_GET['from'] ?? '');
// 前後空白を除去
$from = trim($from);
// GETパラメータ to(終了日)を受け取る
$to = (string)($_GET['to'] ?? '');
// 前後空白を除去
$to = trim($to);
// GETパラメータ type(種類)を受け取る
$type = (string)($_GET['type'] ?? '');
// 前後空白を除去
$type = trim($type);
// 種類のホワイトリスト(表示名つき)
$allowedType = [
'travel' => '出張',
'expense' => '精算',
'purchase' => '購買',
];
// type が許可されていれば採用、違えば空(すべて扱い)
$type = array_key_exists($type, $allowedType) ? $type : '';
// デモ用データ
$rows = [
['id'=>201,'title'=>'出張申請(名古屋)','type'=>'travel','date'=>'2026-01-04','amount'=>12000],
['id'=>202,'title'=>'交通費精算','type'=>'expense','date'=>'2025-12-28','amount'=>3200],
['id'=>203,'title'=>'備品購入(椅子)','type'=>'purchase','date'=>'2025-12-22','amount'=>19800],
['id'=>204,'title'=>'出張申請(福岡)','type'=>'travel','date'=>'2025-12-18','amount'=>22000],
['id'=>205,'title'=>'会議費精算','type'=>'expense','date'=>'2025-12-10','amount'=>5400],
];
// 日付形式(YYYY-MM-DD)かどうかの正規表現
$reDate = '/^\d{4}-\d{2}-\d{2}$/';
// from が空でなく、日付形式でなければ無効化(空に戻す)
if ($from !== '' && !preg_match($reDate, $from)) $from = '';
// to が空でなく、日付形式でなければ無効化(空に戻す)
if ($to !== '' && !preg_match($reDate, $to)) $to = '';
// 条件に合う行だけ残す
$hits = array_values(array_filter($rows, function($r) use ($from, $to, $type) {
// 種類が指定されていて、行の種類と違えば除外
if ($type !== '' && $r['type'] !== $type) return false;
// from が指定されていて、行の日付が from より前なら除外
if ($from !== '' && $r['date'] < $from) return false;
// to が指定されていて、行の日付が to より後なら除外
if ($to !== '' && $r['date'] > $to) return false;
// ここまでOKなら採用
return true;
}));
// 条件の見出しを表示
echo '<div class="RESULT-HEAD">';
// 条件ラベル
echo '<strong>条件:</strong> ';
// 期間の表示(両方空なら指定なし)
echo '期間=' . (
$from === '' && $to === ''
? '(指定なし)'
: h($from ?: '…') . '〜' . h($to ?: '…')
);
// 区切り
echo ' / ';
// 種類の表示(空ならすべて)
echo '種類=' . ($type === '' ? '(すべて)' : h($allowedType[$type]));
// 件数表示
echo ' <strong>' . count($hits) . '件</strong>';
// 見出し終了
echo '</div>';
// 0件ならメッセージを出して終了
if (!$hits) {
// 該当なし表示
echo '<div class="FORM-RESULT is-ng"><strong>該当なし</strong></div>';
// 終了
exit;
}
// テーブル装飾用div開始
echo '<div class="TABLE_WRAPPER">';
// テーブル開始
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 は数値として扱う
$id = (int)$r['id'];
// タイトルは安全に表示
$title = h((string)$r['title']);
// 種類は表示名にして安全に表示
$tp = h($allowedType[$r['type']] ?? (string)$r['type']);
// 日付は安全に表示
$date = h((string)$r['date']);
// 金額はカンマ区切り表示
$amt = number_format((int)$r['amount']);
// 1行出力
echo "<tr><td>{$id}</td><td>{$title}</td><td>{$tp}</td><td>{$date}</td><td>{$amt}</td></tr>";
}
// tbody終了
echo '</tbody>';
// table終了
echo '</table></div>';
// div終了
echo '</div>';
デモ
② 期間で絞る
解説
- フォームの値(
from/to/type)が変わるたびに、hx-getでサーバーへGETリクエストを送ります。 hx-triggerでchange(日付・種類の変更)とreset(条件クリア)を拾い、条件変更のたびに自動で再検索します。hx-includeでフォーム全体をリクエストに含め、開始日・終了日・種類をまとめて送ります。hx-params="from,to,type"で送信するパラメータを絞り、必要な条件だけをサーバーへ渡します。- サーバー(PHP)は日付形式を軽く検証し、from以上 / to以下の範囲でデータを絞り込みます。
- 返ってきた一覧HTMLは
hx-targetで指定した結果エリアに差し替えられ、ページ全体は再読み込みされません。 - このデモでは
hx-push-url="false"にして、フィルタ操作のたびに履歴が増えないようにしています。
③ 権限別に表示を切り替える
権限(閲覧者/管理者など)に応じて、一覧で見せる列や情報量を切り替えるデモです。
同じデータでも「見せてよい範囲」を変えられるので、業務アプリの認可UIの雰囲気を再現できます。
HTML
<div class="DEMO">
<h4>③ 権限別に表示を切り替える</h4>
<form id="DEMO_FILTER_3_FORM" class="FORM">
<label>
権限
<select name="role">
<option value="viewer">閲覧者</option>
<option value="manager">管理者(担当表示)</option>
<option value="admin">管理者(内部列も表示)</option>
</select>
</label>
<button type="reset" class="BTN is-sub">条件クリア</button>
</form>
<div
id="DEMO_FILTER_3_RESULT"
class="RESULT"
aria-live="polite"
hx-get="/htmx/demo/_filter_3.php"
hx-trigger="load, change from:#DEMO_FILTER_3_FORM, reset from:#DEMO_FILTER_3_FORM delay:10ms"
hx-include="#DEMO_FILTER_3_FORM"
hx-params="role"
hx-push-url="false"
hx-target="this"
>
</div>
</div>
PHP
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 返すのはHTML(UTF-8)
header('Content-Type: text/html; charset=UTF-8');
// HTMLに安全に出すための関数
function h(string $s): string {
// HTML特殊文字をエスケープする
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
// GETパラメータ role を受け取る(なければ viewer)
$role = (string)($_GET['role'] ?? 'viewer');
// 前後空白を除去
$role = trim($role);
// 権限のホワイトリスト(表示名つき)
$allowedRole = [
'viewer' => '閲覧者',
'manager' => '管理者(担当表示)',
'admin' => '管理者(内部列も表示)',
];
// role が許可されていれば採用、違えば viewer に戻す
$role = array_key_exists($role, $allowedRole) ? $role : 'viewer';
// 担当者表示用(英→日本語)
$assigneeMap = ['tanaka' => '田中', 'sato' => '佐藤', 'suzuki' => '鈴木'];
// デモ用データ
$rows = [
['id'=>301,'title'=>'稟議:サーバ契約','status'=>'review','date'=>'2026-01-02','assignee'=>'tanaka','internal'=>'見積PDFあり','cost'=>98000],
['id'=>302,'title'=>'交通費精算','status'=>'approved','date'=>'2025-12-27','assignee'=>'sato','internal'=>'領収書OK','cost'=>3200],
['id'=>303,'title'=>'備品購入(モニター)','status'=>'draft','date'=>'2025-12-21','assignee'=>'suzuki','internal'=>'型番要確認','cost'=>24000],
];
// ステータス表示用(英→日本語)
$labelStatus = [
'draft' => '下書き',
'review' => '承認待ち',
'approved' => '承認済み',
'rejected' => '差戻し',
];
// 見出し(現在の権限)を表示
echo '<div class="RESULT-HEAD">';
// 権限ラベル
echo '<strong>権限:</strong> ';
// 権限名を安全に表示
echo h($allowedRole[$role]);
// 見出し終了
echo '</div>';
// テーブル装飾用div開始
echo '<div class="TABLE_WRAPPER">';
// テーブル開始
echo '<table class="TABLE">';
// ヘッダ開始
echo '<thead><tr><th>ID</th><th>タイトル</th><th>状態</th><th>日付</th>';
// viewer 以外なら「担当」列を追加
if ($role !== 'viewer') {
// 担当列
echo '<th>担当</th>';
}
// admin なら内部列も追加
if ($role === 'admin') {
// 金額と内部メモ列
echo '<th>金額</th><th>内部メモ</th>';
}
// ヘッダ行終了
echo '</tr></thead>';
// 本体開始
echo '<tbody>';
// 1行ずつ出力
foreach ($rows as $r) {
// IDは数値として扱う
$id = (int)$r['id'];
// タイトルは安全に表示
$title = h((string)$r['title']);
// ステータスはラベルに変換して安全に表示
$st = h($labelStatus[$r['status']] ?? (string)$r['status']);
// 日付は安全に表示
$date = h((string)$r['date']);
// 基本列(ID/タイトル/状態/日付)
echo "<tr><td>{$id}</td><td>{$title}</td><td>{$st}</td><td>{$date}</td>";
// viewer 以外なら担当も表示
if ($role !== 'viewer') {
// 担当者名を日本語にして安全に表示
$as = h($assigneeMap[$r['assignee']] ?? (string)$r['assignee']);
// 担当列
echo "<td>{$as}</td>";
}
// admin なら内部情報も表示
if ($role === 'admin') {
// 金額をカンマ区切り
$cost = number_format((int)$r['cost']);
// 内部メモを安全に表示
$memo = h((string)$r['internal']);
// 金額・内部メモ列
echo "<td>{$cost}</td><td>{$memo}</td>";
}
// 行を閉じる
echo '</tr>';
}
// tbody終了
echo '</tbody>';
// table終了
echo '</table></div>';
// div終了
echo '</div>';
デモ
③ 権限別に表示を切り替える
解説
- 権限セレクトを切り替えるたびに、
hx-getでサーバーへGETリクエストを送ります。 hx-triggerでchange(権限変更)とreset(初期に戻す)を拾い、操作のたびに自動で再描画します。hx-includeでフォーム全体をリクエストに含め、現在の権限(role)を送ります。hx-params="role"で送信するパラメータを絞り、必要な条件だけをサーバーへ渡します。- サーバー(PHP)は受け取った
roleをホワイトリストで検証し、権限に応じて出力する列(担当/金額/内部メモなど)を切り替えます。 - 返ってきた一覧HTMLは
hx-targetで指定した結果エリアに差し替えられ、ページ全体は再読み込みされません。 - このデモでは
hx-push-url="false"にして、権限切り替えのたびに履歴が増えないようにしています。
参考リンク
このページの著者
経験:Webアプリ/業務システム
得意:PHP・JavaScript・MySQL・CSS
個人実績:フォーム生成基盤/クイズ学習プラットフォーム 等
詳しいプロフィールはこちら! もちもちみかんのプロフィール