htmx 逆引きレシピ
差し替え方を切り替えるには?
hx-target と hx-swap を切り替えると、同じ送信先でも画面の反映方法を変えられます。
このページでは、outerHTML / beforeend / innerHTML を3デモで比較し、hx-preserve で保持する例も確認します。
使用するhtmx属性
タグ:hx-swap / hx-target / hx-select / hx-preserve
hx-target:反映先を指定します(例:closest tr/#DEMO3_MODAL_BODY)。hx-swap:差し替え方式を指定します(outerHTML/beforeend/innerHTML)。hx-select:返却HTMLから採用する断片を選びます。hx-preserve:差し替え時に保持したい要素をID付きで残します。
利用シーン
- テーブル行は outerHTML:申請/請求/ユーザー一覧などで、1行だけ更新して画面全体を揺らさず反映したい(更新ボタン→その行だけ差し替え)
- 一覧は beforeend で追加:検索結果やログ、通知一覧を「もっと読む」で段階表示し、初期表示を軽くしつつ自然に追記したい
- モーダルは innerHTML で差し替え:枠は固定のまま本文だけ切り替え、フォーム入力やボタン位置を維持して“操作感”を崩したくない
共通PHP(全文)
返却する断片は1か所で組み立て、画面側は hx-target と hx-swap の指定だけを切り替えます。
XSS対策は htmlspecialchars を共通関数化して統一しています。
PHP(_swap_strategies_data.php)
/htmx/demo/_swap_strategies_data.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// HTML特殊文字を安全化する
function swap_strategies_h(string $value): string
{
// エスケープ済み文字列を返す
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
// 現在時刻を文字列で返す
function swap_strategies_now(): string
{
// 日本語UI向けの時刻形式で返す
return date('Y-m-d H:i:s');
}
// リクエスト文字列を安全に取り出す
function swap_strategies_request_text(array $source, string $key, string $fallback): string
{
// 生値を文字列として取り出す
$raw = trim((string)($source[$key] ?? ''));
// 空文字なら既定値を返す
if ($raw === '') {
// 既定値を返す
return $fallback;
}
// 入力値を返す
return $raw;
}
// リクエスト整数を安全に取り出す
function swap_strategies_request_int(array $source, string $key, int $fallback): int
{
// 文字列として値を取り出す
$raw = trim((string)($source[$key] ?? ''));
// 整数に解釈できない場合は既定値を返す
if ($raw === '' || filter_var($raw, FILTER_VALIDATE_INT) === false) {
// 既定値を返す
return $fallback;
}
// 整数値として返す
return (int)$raw;
}
// テーブル行の元データを返す
function swap_strategies_base_rows(): array
{
// 行データ配列を返す
return [
['id' => 101, 'title' => '注文A-1001', 'status' => '確認待ち'],
['id' => 102, 'title' => '注文A-1002', 'status' => '発送準備中'],
['id' => 103, 'title' => '注文A-1003', 'status' => '完了'],
];
}
// 初期表示用のテーブル行を返す
function swap_strategies_table_rows(): array
{
// 元データを取得する
$rows = swap_strategies_base_rows();
// 現在時刻を取得する
$now = swap_strategies_now();
// 各行に更新時刻を付与する
foreach ($rows as &$row) {
// 更新時刻を付与する
$row['updated_at'] = $now;
}
unset($row);
// 行一覧を返す
return $rows;
}
// IDで行データを取得する
function swap_strategies_find_row(int $id): ?array
{
// 候補行を順に確認する
foreach (swap_strategies_base_rows() as $row) {
// ID一致時は行を返す
if ((int)$row['id'] === $id) {
// 更新後の見た目に寄せる
$row['status'] = '更新済み';
// 更新時刻を付与する
$row['updated_at'] = swap_strategies_now();
// 行データを返す
return $row;
}
}
// 見つからない場合はnullを返す
return null;
}
// テーブル行HTMLを組み立てる
function swap_strategies_render_table_row(array $row): string
{
// 行IDを取り出す
$id = (int)($row['id'] ?? 0);
// タイトルを安全化する
$titleEsc = swap_strategies_h((string)($row['title'] ?? ''));
// ステータスを安全化する
$statusEsc = swap_strategies_h((string)($row['status'] ?? ''));
// 更新時刻を安全化する
$updatedEsc = swap_strategies_h((string)($row['updated_at'] ?? swap_strategies_now()));
// IDを安全化する
$idEsc = swap_strategies_h((string)$id);
// 行HTMLを返す
return '<tr id="DEMO1_ROW_' . $idEsc . '"><td scope="row">' . $idEsc . '</td><td>' . $titleEsc . '</td><td>' . $statusEsc . '</td><td>' . $updatedEsc . '</td><td><button type="button" hx-get="/htmx/demo/_swap_strategies_api.php?mode=table&id=' . $idEsc . '" hx-target="closest tr" hx-select="tr" hx-swap="outerHTML">更新</button></td></tr>';
}
// 一覧カードの元データを返す
function swap_strategies_list_items(): array
{
// 一覧データ配列を返す
return [
['id' => 1, 'title' => '在庫アラートの見直し', 'summary' => 'しきい値設定を調整して通知件数を最適化します。'],
['id' => 2, 'title' => '配送先の住所補完', 'summary' => '入力補助と確認手順を整理して誤配送を抑えます。'],
['id' => 3, 'title' => '請求CSVの並び順', 'summary' => '現場の確認順に合わせて列順を改善します。'],
['id' => 4, 'title' => '返品理由の分類', 'summary' => 'カテゴリ再編で分析しやすくします。'],
['id' => 5, 'title' => '受注メモのテンプレート', 'summary' => '頻出文言をテンプレ化して入力時間を短縮します。'],
['id' => 6, 'title' => '顧客ランクの再計算', 'summary' => '最新購入履歴でランク更新ルールを検証します。'],
['id' => 7, 'title' => '倉庫別の出荷時間', 'summary' => '平均処理時間を比較してボトルネックを特定します。'],
['id' => 8, 'title' => '再入荷予定の表示', 'summary' => '商品詳細に予定日を出して問い合わせを減らします。'],
['id' => 9, 'title' => '月次集計の締め処理', 'summary' => '締めタイミングとロック手順を標準化します。'],
];
}
// 指定ページ分の一覧データを返す
function swap_strategies_list_page(int $page, int $perPage = 3): array
{
// ページ番号を1以上に補正する
$page = max(1, $page);
// 1ページ件数を1以上に補正する
$perPage = max(1, $perPage);
// 取得開始位置を計算する
$offset = ($page - 1) * $perPage;
// 該当範囲を返す
return array_slice(swap_strategies_list_items(), $offset, $perPage);
}
// 一覧行HTMLを組み立てる
function swap_strategies_render_list_items(array $items, int $page): string
{
// HTML文字列を初期化する
$html = '';
// 各要素をHTMLへ変換する
foreach ($items as $item) {
// 見出しを安全化する
$titleEsc = swap_strategies_h((string)($item['title'] ?? ''));
// 概要を安全化する
$summaryEsc = swap_strategies_h((string)($item['summary'] ?? ''));
// 番号を安全化する
$idEsc = swap_strategies_h((string)($item['id'] ?? ''));
// ページ番号を安全化する
$pageEsc = swap_strategies_h((string)$page);
// 一覧HTMLを連結する
$html .= '<li class="CARD"><h5>#' . $idEsc . ' ' . $titleEsc . '</h5><p>' . $summaryEsc . '</p><p class="HTMX-NOTE">読み込みページ:' . $pageEsc . '</p></li>';
}
// 組み立てたHTMLを返す
return $html;
}
// モーダル本文データを返す
function swap_strategies_modal_panel(string $panel): array
{
// パネル識別子を正規化する
$panel = strtolower(trim($panel));
// 識別子に応じて本文を返す
switch ($panel) {
case 'detail':
// 詳細パネルを返す
return [
'title' => '詳細タブ',
'text' => '注文履歴と対応メモを展開表示しています。',
'hint' => 'innerHTML差し替えでも入力中メモは保持されます。',
];
case 'confirm':
// 確認パネルを返す
return [
'title' => '確認タブ',
'text' => '送信前チェックの項目を表示しています。',
'hint' => '保持対象は id 付き input + hx-preserve です。',
];
default:
// 既定パネルを返す
return [
'title' => '概要タブ',
'text' => 'モーダル枠を固定して本文だけ切り替えます。',
'hint' => 'hx-target="#DEMO3_MODAL_BODY" / hx-swap="innerHTML"',
];
}
}
// モーダル本文HTMLを組み立てる
function swap_strategies_render_modal_body(string $panel): string
{
// パネル情報を取得する
$data = swap_strategies_modal_panel($panel);
// タイトルを安全化する
$titleEsc = swap_strategies_h((string)$data['title']);
// 本文を安全化する
$textEsc = swap_strategies_h((string)$data['text']);
// 補足を安全化する
$hintEsc = swap_strategies_h((string)$data['hint']);
// 更新時刻を安全化する
$nowEsc = swap_strategies_h(swap_strategies_now());
// モーダル本文HTMLを返す
return '<div class="CARD"><h5>' . $titleEsc . '</h5><p>' . $textEsc . '</p><label>メモ(入力内容は保持されます)<input id="DEMO3_PRESERVE_NOTE" name="note" type="text" value="" placeholder="ここに入力してからタブ切替" hx-preserve></label><p class="HTMX-NOTE">' . $hintEsc . ' / 更新:' . $nowEsc . '</p><div class="FLEX ROWGAP8 COLUMNGAP8"><button type="button" class="BTN BTN_SUB" hx-get="/htmx/demo/_swap_strategies_api.php?mode=modal&panel=overview" hx-target="#DEMO3_MODAL_BODY" hx-swap="innerHTML">概要</button><button type="button" class="BTN BTN_SUB" hx-get="/htmx/demo/_swap_strategies_api.php?mode=modal&panel=detail" hx-target="#DEMO3_MODAL_BODY" hx-swap="innerHTML">詳細</button><button type="button" class="BTN BTN_SUB" hx-get="/htmx/demo/_swap_strategies_api.php?mode=modal&panel=confirm" hx-target="#DEMO3_MODAL_BODY" hx-swap="innerHTML">確認</button></div></div>';
}
① テーブル行は outerHTML で行だけ入れ替える
更新ボタンは同じAPIへ hx-get し、対象行を hx-target="closest tr" で指定します。
hx-swap="outerHTML" なので、行単位で置換されます。
HTML(view全文)
_demo_htmx_1.php
<div class="DEMO">
<h4>テーブル行は <code>outerHTML</code> で更新</h4>
<p class="HTMX-NOTE">
更新ボタンは行自身を <code>hx-target="closest tr"</code> で指定し、<code>hx-swap="outerHTML"</code> で行ごと差し替えます。
</p>
<?php require("{$_SERVER['DOCUMENT_ROOT']}/htmx/demo/_swap_strategies_1.php"); ?>
</div>
PHP(送信先)
/htmx/demo/_swap_strategies_1.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 共通関数を読み込む
require_once(__DIR__ . '/_swap_strategies_data.php');
// 初期行データを取得する
$rows = swap_strategies_table_rows();
// 行HTMLを初期化する
$rowsHtml = '';
// 各行をHTMLへ変換する
foreach ($rows as $row) {
// 行HTMLを連結する
$rowsHtml .= swap_strategies_render_table_row($row) . "\n";
}
?>
<div class="TABLE_WRAPPER">
<table>
<thead>
<tr>
<th class="W50">ID</th>
<th class="W125">注文名</th>
<th class="W100">状態</th>
<th>更新時刻</th>
<th class="W75">操作</th>
</tr>
</thead>
<tbody>
<?= $rowsHtml ?>
</tbody>
</table>
</div>
/htmx/demo/_swap_strategies_api.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// HTML断片として返す
header('Content-Type: text/html; charset=UTF-8');
// 共通関数を読み込む
require_once(__DIR__ . '/_swap_strategies_data.php');
// モードを取り出す
$mode = swap_strategies_request_text($_GET, 'mode', 'table');
// tableモード時は行断片を返す
if ($mode === 'table') {
// 行IDを受け取る
$id = swap_strategies_request_int($_GET, 'id', 101);
// 対象行を取得する
$row = swap_strategies_find_row($id);
// 見つかった行を返す
if ($row !== null) {
// 行断片を返す
echo swap_strategies_render_table_row($row);
// 返却を終了する
exit;
}
// 対象なしの行断片を返す
echo '<tr><td colspan="5">対象行が見つかりませんでした。</td></tr>';
// 返却を終了する
exit;
}
// listモード時は追加行断片を返す
if ($mode === 'list') {
// ページ番号を受け取る
$page = swap_strategies_request_int($_GET, 'page', 2);
// 追加分を取得する
$items = swap_strategies_list_page($page);
// 追加分が無い場合は案内を返す
if ($items === []) {
// 読み込み完了メッセージを返す
echo '<li class="CARD"><p>追加できる項目はありません。</p></li>';
// 返却を終了する
exit;
}
// 追加分のli断片を返す
echo swap_strategies_render_list_items($items, $page);
// 返却を終了する
exit;
}
// modalモード時は本文断片を返す
if ($mode === 'modal') {
// パネル識別子を受け取る
$panel = swap_strategies_request_text($_GET, 'panel', 'overview');
// 本文断片を返す
echo swap_strategies_render_modal_body($panel);
// 返却を終了する
exit;
}
// 不明モード時のメッセージを返す
echo '<p class="HTMX-NOTE">mode は table / list / modal を指定してください。</p>';
デモ
テーブル行は outerHTML で更新
更新ボタンは行自身を hx-target="closest tr" で指定し、hx-swap="outerHTML" で行ごと差し替えます。
| ID | 注文名 | 状態 | 更新時刻 | 操作 |
|---|---|---|---|---|
| 101 | 注文A-1001 | 確認待ち | 2026-02-26 09:40:40 | |
| 102 | 注文A-1002 | 発送準備中 | 2026-02-26 09:40:40 | |
| 103 | 注文A-1003 | 完了 | 2026-02-26 09:40:40 |
解説
- 返却HTMLは1行断片ですが、
outerHTML指定で行全体を入れ替えます。 - テーブル全体を再描画しないため、局所更新を明確にできます。
② 一覧は beforeend で末尾へ追加する
「もっと読む」ボタンは追加分の <li> だけ受け取り、#DEMO2_LIST の末尾へ挿入します。
同じAPIでも hx-swap を変えるだけで append 挙動になります。
HTML(view全文)
_demo_htmx_2.php
<div class="DEMO">
<h4>一覧は <code>beforeend</code> で追加</h4>
<p class="HTMX-NOTE">
同じ送信先でも、<code>hx-target="#DEMO2_LIST"</code> と <code>hx-swap="beforeend"</code> で「末尾追加」挙動になります。
</p>
<?php require("{$_SERVER['DOCUMENT_ROOT']}/htmx/demo/_swap_strategies_2.php"); ?>
</div>
PHP(送信先)
/htmx/demo/_swap_strategies_2.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 共通関数を読み込む
require_once(__DIR__ . '/_swap_strategies_data.php');
// 初回ページ番号を決める
$firstPage = 1;
// 初回表示分を取得する
$items = swap_strategies_list_page($firstPage);
?>
<ul id="DEMO2_LIST" class="HTMX-LIST">
<?= swap_strategies_render_list_items($items, $firstPage) ?>
</ul>
<button
type="button"
class="BTN"
hx-get="/htmx/demo/_swap_strategies_api.php?mode=list&page=2"
hx-target="#DEMO2_LIST"
hx-select="li"
hx-swap="beforeend"
>
もっと読む(3件追加)
</button>
/htmx/demo/_swap_strategies_api.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// HTML断片として返す
header('Content-Type: text/html; charset=UTF-8');
// 共通関数を読み込む
require_once(__DIR__ . '/_swap_strategies_data.php');
// モードを取り出す
$mode = swap_strategies_request_text($_GET, 'mode', 'table');
// tableモード時は行断片を返す
if ($mode === 'table') {
// 行IDを受け取る
$id = swap_strategies_request_int($_GET, 'id', 101);
// 対象行を取得する
$row = swap_strategies_find_row($id);
// 見つかった行を返す
if ($row !== null) {
// 行断片を返す
echo swap_strategies_render_table_row($row);
// 返却を終了する
exit;
}
// 対象なしの行断片を返す
echo '<tr><td colspan="5">対象行が見つかりませんでした。</td></tr>';
// 返却を終了する
exit;
}
// listモード時は追加行断片を返す
if ($mode === 'list') {
// ページ番号を受け取る
$page = swap_strategies_request_int($_GET, 'page', 2);
// 追加分を取得する
$items = swap_strategies_list_page($page);
// 追加分が無い場合は案内を返す
if ($items === []) {
// 読み込み完了メッセージを返す
echo '<li class="CARD"><p>追加できる項目はありません。</p></li>';
// 返却を終了する
exit;
}
// 追加分のli断片を返す
echo swap_strategies_render_list_items($items, $page);
// 返却を終了する
exit;
}
// modalモード時は本文断片を返す
if ($mode === 'modal') {
// パネル識別子を受け取る
$panel = swap_strategies_request_text($_GET, 'panel', 'overview');
// 本文断片を返す
echo swap_strategies_render_modal_body($panel);
// 返却を終了する
exit;
}
// 不明モード時のメッセージを返す
echo '<p class="HTMX-NOTE">mode は table / list / modal を指定してください。</p>';
デモ
一覧は beforeend で追加
同じ送信先でも、hx-target="#DEMO2_LIST" と hx-swap="beforeend" で「末尾追加」挙動になります。
#1 在庫アラートの見直し
しきい値設定を調整して通知件数を最適化します。
読み込みページ:1
#2 配送先の住所補完
入力補助と確認手順を整理して誤配送を抑えます。
読み込みページ:1
#3 請求CSVの並び順
現場の確認順に合わせて列順を改善します。
読み込みページ:1
解説
beforeendは既存リストを残したまま、返却断片を末尾へ積み増します。- 「もっと読む」など追記UIの基本パターンとして使えます。
③ モーダルは innerHTML で本文だけ差し替える(hx-preserve 併用)
モーダル枠は固定し、本文領域 #DEMO3_MODAL_BODY だけを差し替えます。
本文内の入力欄は id="DEMO3_PRESERVE_NOTE" + hx-preserve で保持します。
HTML(view全文)
_demo_htmx_3.php
<div class="DEMO">
<h4>モーダル本文は <code>innerHTML</code> で差し替え</h4>
<p class="HTMX-NOTE">
モーダル枠は固定し、本文だけ <code>#DEMO3_MODAL_BODY</code> へ反映します。<br>
入力欄は <code>id + hx-preserve</code> で保持します。
</p>
<section id="DEMO3_MODAL" class="CARD" role="dialog" aria-modal="true" aria-labelledby="DEMO3_MODAL_TITLE">
<header class="CARD">
<h5 id="DEMO3_MODAL_TITLE">注文メモモーダル(枠は固定)</h5>
</header>
<div id="DEMO3_MODAL_BODY" class="CARD" aria-live="polite">
<?php require("{$_SERVER['DOCUMENT_ROOT']}/htmx/demo/_swap_strategies_3.php"); ?>
</div>
</section>
</div>
PHP(送信先)
/htmx/demo/_swap_strategies_3.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 共通関数を読み込む
require_once(__DIR__ . '/_swap_strategies_data.php');
// 初期表示パネルを決める
$panel = 'overview';
// モーダル本文を出力する
echo swap_strategies_render_modal_body($panel);
/htmx/demo/_swap_strategies_api.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// HTML断片として返す
header('Content-Type: text/html; charset=UTF-8');
// 共通関数を読み込む
require_once(__DIR__ . '/_swap_strategies_data.php');
// モードを取り出す
$mode = swap_strategies_request_text($_GET, 'mode', 'table');
// tableモード時は行断片を返す
if ($mode === 'table') {
// 行IDを受け取る
$id = swap_strategies_request_int($_GET, 'id', 101);
// 対象行を取得する
$row = swap_strategies_find_row($id);
// 見つかった行を返す
if ($row !== null) {
// 行断片を返す
echo swap_strategies_render_table_row($row);
// 返却を終了する
exit;
}
// 対象なしの行断片を返す
echo '<tr><td colspan="5">対象行が見つかりませんでした。</td></tr>';
// 返却を終了する
exit;
}
// listモード時は追加行断片を返す
if ($mode === 'list') {
// ページ番号を受け取る
$page = swap_strategies_request_int($_GET, 'page', 2);
// 追加分を取得する
$items = swap_strategies_list_page($page);
// 追加分が無い場合は案内を返す
if ($items === []) {
// 読み込み完了メッセージを返す
echo '<li class="CARD"><p>追加できる項目はありません。</p></li>';
// 返却を終了する
exit;
}
// 追加分のli断片を返す
echo swap_strategies_render_list_items($items, $page);
// 返却を終了する
exit;
}
// modalモード時は本文断片を返す
if ($mode === 'modal') {
// パネル識別子を受け取る
$panel = swap_strategies_request_text($_GET, 'panel', 'overview');
// 本文断片を返す
echo swap_strategies_render_modal_body($panel);
// 返却を終了する
exit;
}
// 不明モード時のメッセージを返す
echo '<p class="HTMX-NOTE">mode は table / list / modal を指定してください。</p>';
デモ
モーダル本文は innerHTML で差し替え
モーダル枠は固定し、本文だけ #DEMO3_MODAL_BODY へ反映します。
入力欄は id + hx-preserve で保持します。
注文メモモーダル(枠は固定)
概要タブ
モーダル枠を固定して本文だけ切り替えます。
hx-target="#DEMO3_MODAL_BODY" / hx-swap="innerHTML" / 更新:2026-02-26 09:40:40
解説
- 固定枠と可変本文を分離すると、モーダルの責務が明確になります。
hx-preserveで入力途中の値を保持しつつ、本文情報だけ更新できます。
次に読むオススメレシピ
参考リンク
このページの著者
経験:Webアプリ/業務システム
得意:PHP・JavaScript・MySQL・CSS
個人実績:フォーム生成基盤/クイズ学習プラットフォーム 等
詳しいプロフィールはこちら! もちもちみかんのプロフィール