htmx 逆引きレシピ
テーブルをソートするには?
公開日:
最終更新日:
テーブルの一覧は、日付・金額・ステータスなどで並び替えできると、探す手間が一気に減ります。
htmxなら、ヘッダクリックで必要部分だけ再描画しつつ、URLにも状態を残せるソートUIが手軽に作れます。
このページでは「最小構成」「ヘッダクリックで再描画」「ソートをURLにも反映」の3例で、
hx-get / hx-vals / hx-target / hx-push-url を使った実装を解説します。
使用するhtmx属性
hx-get:ヘッダクリックで並び替え条件をGETで送り、テーブルHTMLを取得するhx-vals:リンクのクエリに依存せず、送るパラメータ(sort/dir)を明示するhx-target:返ってきたHTMLを差し替える先(テーブルコンテナ)を指定するhx-push-url:並び替え状態をURLに反映して、リロード/直リンクに対応する
利用シーン
- 「日付/金額/ステータスで並び替え」:請求・申請・発注などの一覧を用途別に見やすくしたい
- 「ヘッダクリックで再描画」:ページ全体は動かさず、テーブル部分だけ更新したい
- 「URLにも反映」:並び替え状態をURLに残し、リロード/共有で同じ並びを再現したい
共通PHP
PHP(_table_sort_data.php)
<?php
// HTMLエスケープ関数を用意する
function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); }
// デモ用の行データを用意する
$TABLE_SORT_ROWS = [
['id' => 501, 'title' => '請求書の発行', 'date' => '2026-01-18', 'amount' => 12800, 'status' => 'processing'],
['id' => 502, 'title' => '備品購入(マウス)', 'date' => '2026-01-12', 'amount' => 2800, 'status' => 'done'],
['id' => 503, 'title' => '出張申請(札幌)', 'date' => '2026-01-09', 'amount' => 42000, 'status' => 'pending'],
['id' => 504, 'title' => '契約更新の確認', 'date' => '2026-01-05', 'amount' => 0, 'status' => 'processing'],
['id' => 505, 'title' => '交通費精算', 'date' => '2025-12-28', 'amount' => 3600, 'status' => 'done'],
['id' => 506, 'title' => '研修キャンセル', 'date' => '2025-12-26', 'amount' => 15000, 'status' => 'canceled'],
['id' => 507, 'title' => '請求書の再発行', 'date' => '2025-12-20', 'amount' => 12800, 'status' => 'pending'],
];
// ステータスの表示ラベル
$TABLE_SORT_STATUS_LABELS = [
'pending' => '未処理',
'processing' => '処理中',
'done' => '完了',
'canceled' => 'キャンセル',
];
// ステータスの並び順(小さいほど優先)
$TABLE_SORT_STATUS_ORDER = [
'pending' => 1,
'processing' => 2,
'done' => 3,
'canceled' => 4,
];
// sort / dir を取得して検証する関数
function table_sort_get_params(array $get): array {
// sortを取得する(デフォルトはdate)
$sort = (string)($get['sort'] ?? 'date');
// dirを取得する(デフォルトはdesc)
$dir = (string)($get['dir'] ?? 'desc');
// 前後空白を除去する
$sort = trim($sort);
$dir = trim($dir);
// 許可された値だけを受け付ける
$allowedSort = ['date', 'amount', 'status'];
$allowedDir = ['asc', 'desc'];
// sortが不正ならデフォルト
if (!in_array($sort, $allowedSort, true)) $sort = 'date';
// dirが不正ならデフォルト
if (!in_array($dir, $allowedDir, true)) $dir = 'desc';
// 検証済みを返す
return [$sort, $dir];
}
// 並び替えを行う関数
function table_sort_sort_rows(array $rows, string $sort, string $dir, array $statusOrder): array {
// 昇順/降順の係数
$mul = ($dir === 'desc') ? -1 : 1;
// 並び替え
usort($rows, function(array $a, array $b) use ($sort, $mul, $statusOrder): int {
// 比較結果
$cmp = 0;
// 列ごとの比較
switch ($sort) {
case 'amount':
$cmp = ((int)$a['amount']) <=> ((int)$b['amount']);
break;
case 'status':
$cmp = ((int)($statusOrder[$a['status']] ?? 999)) <=> ((int)($statusOrder[$b['status']] ?? 999));
break;
case 'date':
default:
$cmp = strtotime((string)$a['date']) <=> strtotime((string)$b['date']);
break;
}
// 同値ならIDで安定化する
if ($cmp === 0) {
$cmp = ((int)$a['id']) <=> ((int)$b['id']);
}
// 昇降順を反映して返す
return $cmp * $mul;
});
// 並び替え済みを返す
return array_values($rows);
}
// aria-sort を組み立てる関数
function table_sort_aria_sort(string $column, string $sort, string $dir): string {
// 現在列以外はnone
if ($column !== $sort) return 'none';
// 現在列の昇降順
return ($dir === 'asc') ? 'ascending' : 'descending';
}
// 見出しの矢印を返す関数
function table_sort_indicator(string $column, string $sort, string $dir): string {
// 現在列以外は空
if ($column !== $sort) return '';
// 昇降順の矢印
return ($dir === 'asc')
? ' <span aria-hidden="true">▲</span>'
: ' <span aria-hidden="true">▼</span>';
}
// 次の並び順を返す関数
function table_sort_next_dir(string $column, string $sort, string $dir): string {
// 同じ列なら昇降順を反転
if ($column === $sort) return ($dir === 'asc') ? 'desc' : 'asc';
// 別列なら昇順から
return 'asc';
}
① 日付/金額/ステータスで並び替え
ヘッダリンクにhx-getを付け、結果テーブルだけ差し替える最小構成です。
まずは「並び替えできること」を一番シンプルに確認したいときに向きます。
HTML
<div class="DEMO">
<h4>① 日付/金額/ステータスで並び替え</h4>
<div
id="DEMO_SORT_1_RESULT"
class="RESULT"
aria-live="polite"
hx-get="/htmx/demo/_table_sort_1.php"
hx-trigger="load"
hx-target="this"
>
<span class="HTMX-NOTE">読み込み中...</span>
</div>
</div>
PHP
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 返すのはHTML(UTF-8)
header('Content-Type: text/html; charset=UTF-8');
// 共通データ/関数を読み込む
require __DIR__ . '/_table_sort_data.php';
// sort / dir を取得して検証する
[$sort, $dir] = table_sort_get_params($_GET);
// 並び替え済みの行を取得する
$rows = table_sort_sort_rows($TABLE_SORT_ROWS, $sort, $dir, $TABLE_SORT_STATUS_ORDER);
// 表示用ラベル
$sortLabels = ['date' => '日付', 'amount' => '金額', 'status' => 'ステータス'];
$dirLabels = ['asc' => '昇順', 'desc' => '降順'];
// 見出しを表示する
echo '<div class="RESULT-HEAD">';
echo '<strong>並び替え:</strong> ' . h($sortLabels[$sort]) . '(' . h($dirLabels[$dir]) . ')';
echo ' <strong>' . count($rows) . '件</strong>';
echo '</div>';
// テーブル開始
echo '<div class="TABLE_WRAPPER">';
echo '<table class="TABLE">';
echo '<thead><tr>';
// ヘッダ(並び替えリンク)
$targetId = 'DEMO_SORT_1_RESULT';
$baseUrl = '/htmx/demo/_table_sort_1.php';
$nextDate = table_sort_next_dir('date', $sort, $dir);
$nextAmt = table_sort_next_dir('amount', $sort, $dir);
$nextSt = table_sort_next_dir('status', $sort, $dir);
$urlDate = $baseUrl . '?sort=date&dir=' . $nextDate;
$urlAmt = $baseUrl . '?sort=amount&dir=' . $nextAmt;
$urlSt = $baseUrl . '?sort=status&dir=' . $nextSt;
$ariaDate = table_sort_aria_sort('date', $sort, $dir);
$ariaAmt = table_sort_aria_sort('amount', $sort, $dir);
$ariaSt = table_sort_aria_sort('status', $sort, $dir);
$iconDate = table_sort_indicator('date', $sort, $dir);
$iconAmt = table_sort_indicator('amount', $sort, $dir);
$iconSt = table_sort_indicator('status', $sort, $dir);
// 日付
echo '<th aria-sort="' . h($ariaDate) . '">'
. '<a href="' . h($urlDate) . '" hx-get="' . h($urlDate) . '" hx-target="#' . h($targetId) . '">日付' . $iconDate . '</a>'
. '</th>';
// 内容(固定列)
echo '<th>内容</th>';
// 金額
echo '<th aria-sort="' . h($ariaAmt) . '">'
. '<a href="' . h($urlAmt) . '" hx-get="' . h($urlAmt) . '" hx-target="#' . h($targetId) . '">金額' . $iconAmt . '</a>'
. '</th>';
// ステータス
echo '<th aria-sort="' . h($ariaSt) . '">'
. '<a href="' . h($urlSt) . '" hx-get="' . h($urlSt) . '" hx-target="#' . h($targetId) . '">ステータス' . $iconSt . '</a>'
. '</th>';
// ヘッダ終了
echo '</tr></thead>';
// 本体開始
echo '<tbody>';
// 1行ずつ表示
foreach ($rows as $r) {
// 日付
$date = h((string)$r['date']);
// 内容
$title = h((string)$r['title']);
// 金額
$amount = h(number_format((int)$r['amount']));
// ステータス
$status = h($TABLE_SORT_STATUS_LABELS[$r['status']] ?? (string)$r['status']);
// 1行出力
echo "<tr><td>{$date}</td><td>{$title}</td><td>{$amount}</td><td>{$status}</td></tr>";
}
// tbody終了
echo '</tbody>';
// table終了
echo '</table></div>';
デモ
① 日付/金額/ステータスで並び替え
読み込み中...
解説
- ヘッダのリンクに
hx-getを付け、クリックで並び替え結果のHTMLを取得します。 hx-targetで結果エリアを指定し、テーブル部分だけを差し替えます。- PHP側は
sortとdirを検証し、日付/金額/ステータスを安全に並び替えます。 - 現在の並び替え列は
aria-sortと▲▼で示し、UI状態を明示します。
② ヘッダクリックで再描画
ヘッダのリンクURLとは独立して、送信するパラメータをhx-valsで明示する例です。
クエリ直書きに依存しないので、UI側の表示URLを自由に設計できます。
HTML
<div class="DEMO">
<h4>② ヘッダクリックで再描画</h4>
<div
id="DEMO_SORT_2_RESULT"
class="RESULT"
aria-live="polite"
hx-get="/htmx/demo/_table_sort_2.php"
hx-trigger="load"
hx-target="this"
>
<span class="HTMX-NOTE">読み込み中...</span>
</div>
</div>
PHP
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 返すのはHTML(UTF-8)
header('Content-Type: text/html; charset=UTF-8');
// 共通データ/関数を読み込む
require __DIR__ . '/_table_sort_data.php';
// sort / dir を取得して検証する
[$sort, $dir] = table_sort_get_params($_GET);
// 並び替え済みの行を取得する
$rows = table_sort_sort_rows($TABLE_SORT_ROWS, $sort, $dir, $TABLE_SORT_STATUS_ORDER);
// 表示用ラベル
$sortLabels = ['date' => '日付', 'amount' => '金額', 'status' => 'ステータス'];
$dirLabels = ['asc' => '昇順', 'desc' => '降順'];
// 見出しを表示する
echo '<div class="RESULT-HEAD">';
echo '<strong>並び替え:</strong> ' . h($sortLabels[$sort]) . '(' . h($dirLabels[$dir]) . ')';
echo ' <strong>' . count($rows) . '件</strong>';
echo '</div>';
// テーブル開始
echo '<div class="TABLE_WRAPPER">';
echo '<table class="TABLE">';
echo '<thead><tr>';
// ヘッダ(hx-valsで送信)
$targetId = 'DEMO_SORT_2_RESULT';
$baseUrl = '/htmx/demo/_table_sort_2.php';
$nextDate = table_sort_next_dir('date', $sort, $dir);
$nextAmt = table_sort_next_dir('amount', $sort, $dir);
$nextSt = table_sort_next_dir('status', $sort, $dir);
$urlDate = $baseUrl . '?sort=date&dir=' . $nextDate;
$urlAmt = $baseUrl . '?sort=amount&dir=' . $nextAmt;
$urlSt = $baseUrl . '?sort=status&dir=' . $nextSt;
$ariaDate = table_sort_aria_sort('date', $sort, $dir);
$ariaAmt = table_sort_aria_sort('amount', $sort, $dir);
$ariaSt = table_sort_aria_sort('status', $sort, $dir);
$iconDate = table_sort_indicator('date', $sort, $dir);
$iconAmt = table_sort_indicator('amount', $sort, $dir);
$iconSt = table_sort_indicator('status', $sort, $dir);
$valsDate = h(json_encode(['sort' => 'date', 'dir' => $nextDate], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$valsAmt = h(json_encode(['sort' => 'amount', 'dir' => $nextAmt ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$valsSt = h(json_encode(['sort' => 'status', 'dir' => $nextSt ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
// 日付
echo '<th aria-sort="' . h($ariaDate) . '">'
. '<a href="' . h($urlDate) . '" hx-get="' . h($baseUrl) . '" hx-vals=\'' . $valsDate . '\' hx-target="#' . h($targetId) . '">日付' . $iconDate . '</a>'
. '</th>';
// 内容(固定列)
echo '<th>内容</th>';
// 金額
echo '<th aria-sort="' . h($ariaAmt) . '">'
. '<a href="' . h($urlAmt) . '" hx-get="' . h($baseUrl) . '" hx-vals=\'' . $valsAmt . '\' hx-target="#' . h($targetId) . '">金額' . $iconAmt . '</a>'
. '</th>';
// ステータス
echo '<th aria-sort="' . h($ariaSt) . '">'
. '<a href="' . h($urlSt) . '" hx-get="' . h($baseUrl) . '" hx-vals=\'' . $valsSt . '\' hx-target="#' . h($targetId) . '">ステータス' . $iconSt . '</a>'
. '</th>';
// ヘッダ終了
echo '</tr></thead>';
// 本体開始
echo '<tbody>';
// 1行ずつ表示
foreach ($rows as $r) {
// 日付
$date = h((string)$r['date']);
// 内容
$title = h((string)$r['title']);
// 金額
$amount = h(number_format((int)$r['amount']));
// ステータス
$status = h($TABLE_SORT_STATUS_LABELS[$r['status']] ?? (string)$r['status']);
// 1行出力
echo "<tr><td>{$date}</td><td>{$title}</td><td>{$amount}</td><td>{$status}</td></tr>";
}
// tbody終了
echo '</tbody>';
// table終了
echo '</table></div>';
デモ
② ヘッダクリックで再描画
読み込み中...
解説
- ヘッダリンクのURLはそのまま残し、送信する値だけを
hx-valsで指定しています。 - クエリ直書きに依存しないため、URL設計を変えても送信値の制御が簡単です。
- PHP側は
sort/dirをホワイトリスト検証し、許可された値だけでソートします。 hx-targetでテーブル部分だけを更新し、ページ全体は再描画しません。
③ ソートをURLにも反映
並び替え状態(sort/dir)をURLに反映し、リロード/直リンクでも同じ状態を再現します。
ヘッダクリック時にURLを書き換え、初回ロード時はURLの値を読み取って表示します。
HTML
<div class="DEMO">
<h4>③ ソートをURLにも反映</h4>
<div
id="DEMO_SORT_3_RESULT"
class="RESULT"
aria-live="polite"
hx-get="/htmx/demo/_table_sort_3.php"
hx-trigger="load"
hx-vals='js:{sort:(new URLSearchParams(location.search).get("sort")||"date"),dir:(new URLSearchParams(location.search).get("dir")||"desc")}'
hx-target="this"
>
<span class="HTMX-NOTE">読み込み中...</span>
</div>
</div>
PHP
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 返すのはHTML(UTF-8)
header('Content-Type: text/html; charset=UTF-8');
// 共通データ/関数を読み込む
require __DIR__ . '/_table_sort_data.php';
// sort / dir を取得して検証する
[$sort, $dir] = table_sort_get_params($_GET);
// 並び替え済みの行を取得する
$rows = table_sort_sort_rows($TABLE_SORT_ROWS, $sort, $dir, $TABLE_SORT_STATUS_ORDER);
// 表示用ラベル
$sortLabels = ['date' => '日付', 'amount' => '金額', 'status' => 'ステータス'];
$dirLabels = ['asc' => '昇順', 'desc' => '降順'];
// 見出しを表示する
echo '<div class="RESULT-HEAD">';
echo '<strong>並び替え:</strong> ' . h($sortLabels[$sort]) . '(' . h($dirLabels[$dir]) . ')';
echo ' <strong>' . count($rows) . '件</strong>';
echo '</div>';
// テーブル開始
echo '<div class="TABLE_WRAPPER">';
echo '<table class="TABLE">';
echo '<thead><tr>';
// ヘッダ(hx-push-urlでURL更新)
$targetId = 'DEMO_SORT_3_RESULT';
$baseUrl = '/htmx/demo/_table_sort_3.php';
$nextDate = table_sort_next_dir('date', $sort, $dir);
$nextAmt = table_sort_next_dir('amount', $sort, $dir);
$nextSt = table_sort_next_dir('status', $sort, $dir);
$pageDate = '?sort=date&dir=' . $nextDate;
$pageAmt = '?sort=amount&dir=' . $nextAmt;
$pageSt = '?sort=status&dir=' . $nextSt;
$demoDate = $baseUrl . $pageDate;
$demoAmt = $baseUrl . $pageAmt;
$demoSt = $baseUrl . $pageSt;
$ariaDate = table_sort_aria_sort('date', $sort, $dir);
$ariaAmt = table_sort_aria_sort('amount', $sort, $dir);
$ariaSt = table_sort_aria_sort('status', $sort, $dir);
$iconDate = table_sort_indicator('date', $sort, $dir);
$iconAmt = table_sort_indicator('amount', $sort, $dir);
$iconSt = table_sort_indicator('status', $sort, $dir);
// 日付
echo '<th aria-sort="' . h($ariaDate) . '">'
. '<a href="' . h($pageDate) . '" hx-get="' . h($demoDate) . '" hx-target="#' . h($targetId) . '" hx-push-url="' . h($pageDate) . '">日付' . $iconDate . '</a>'
. '</th>';
// 内容(固定列)
echo '<th>内容</th>';
// 金額
echo '<th aria-sort="' . h($ariaAmt) . '">'
. '<a href="' . h($pageAmt) . '" hx-get="' . h($demoAmt) . '" hx-target="#' . h($targetId) . '" hx-push-url="' . h($pageAmt) . '">金額' . $iconAmt . '</a>'
. '</th>';
// ステータス
echo '<th aria-sort="' . h($ariaSt) . '">'
. '<a href="' . h($pageSt) . '" hx-get="' . h($demoSt) . '" hx-target="#' . h($targetId) . '" hx-push-url="' . h($pageSt) . '">ステータス' . $iconSt . '</a>'
. '</th>';
// ヘッダ終了
echo '</tr></thead>';
// 本体開始
echo '<tbody>';
// 1行ずつ表示
foreach ($rows as $r) {
// 日付
$date = h((string)$r['date']);
// 内容
$title = h((string)$r['title']);
// 金額
$amount = h(number_format((int)$r['amount']));
// ステータス
$status = h($TABLE_SORT_STATUS_LABELS[$r['status']] ?? (string)$r['status']);
// 1行出力
echo "<tr><td>{$date}</td><td>{$title}</td><td>{$amount}</td><td>{$status}</td></tr>";
}
// tbody終了
echo '</tbody>';
// table終了
echo '</table></div>';
デモ
③ ソートをURLにも反映
読み込み中...
解説
hx-push-urlでURLに?sort=...&dir=...を反映し、ブラウザ履歴と共有を可能にします。- 初回ロード時は
hx-valsのjs:でURLのクエリを読み取り、同じ並び替え状態で表示します。 - ヘッダリンクは「表示用URL」と「取得用URL」を分け、結果は
hx-targetで差し替えます。 - これにより、リロード/直リンクでも同じ並び順を再現できます。
次に読むオススメレシピ
参考リンク
このページの著者
経験:Webアプリ/業務システム
得意:PHP・JavaScript・MySQL・CSS
個人実績:フォーム生成基盤/クイズ学習プラットフォーム 等
詳しいプロフィールはこちら! もちもちみかんのプロフィール