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>';

デモ

① 担当者/ステータスで絞る

ステータス(複数選択OK)

解説

  • フォームの値(担当者・ステータス)が変わるたびに、hx-getでサーバーへGETリクエストを送ります。
  • hx-triggerchange(選択/チェック変更)と 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-triggerchange(日付・種類の変更)と 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-triggerchange(権限変更)と reset(初期に戻す)を拾い、操作のたびに自動で再描画します。
  • hx-includeでフォーム全体をリクエストに含め、現在の権限(role)を送ります。
  • hx-params="role"で送信するパラメータを絞り、必要な条件だけをサーバーへ渡します。
  • サーバー(PHP)は受け取ったroleをホワイトリストで検証し、権限に応じて出力する列(担当/金額/内部メモなど)を切り替えます。
  • 返ってきた一覧HTMLはhx-targetで指定した結果エリアに差し替えられ、ページ全体は再読み込みされません。
  • このデモではhx-push-url="false"にして、権限切り替えのたびに履歴が増えないようにしています。

このページの著者

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

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

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

得意:PHP・JavaScript・MySQL・CSS

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

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

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

もちもちみかん.comとは


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

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