htmx 逆引きレシピ
差し替え方を切り替えるには?

公開日:
最終更新日:

hx-targethx-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-targethx-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&amp;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&amp;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&amp;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&amp;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&amp;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-preserve で入力途中の値を保持しつつ、本文情報だけ更新できます。

次に読むオススメレシピ

このページの著者

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

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

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

得意:PHP・JavaScript・MySQL・CSS

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

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

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

もちもちみかん.comとは


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

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