htmx 逆引きレシピ
リンク/フォームをboostして“準SPA”にするには?

公開日:
最終更新日:

既存のSSRサイトでも、ページ遷移のたびに全体が再読み込みされると「待ち」が増えてストレスになります。
htmxの hx-boost を使うと、リンククリックやフォーム送信を“部分更新”に変えて、準SPAのような体験にできます。

このページでは「既存SSRに後付け」「遷移を速く」「段階的にhtmxへ寄せる」の3デモを用意しました。
hx-select / hx-target / hx-swap と組み合わせて、業務UIでもそのまま使える形に落とし込みます。

使用するhtmx属性

  • hx-boost:リンククリック/フォーム送信をAJAX化し、ページ遷移を“準SPA”っぽくする
  • hx-select:レスポンスHTMLから「必要な部分だけ」を抜き出して使う(例:#MAINだけ)
  • hx-target:差し替える先(どこに反映するか)を指定する
  • hx-swap:差し替え方を指定する(例:outerHTMLで丸ごと置換)

利用シーン

  • 「既存SSRに後付け」:まずは“全部作り直さずに”部分遷移だけ導入したい
  • 「遷移を速く」:一覧→詳細、検索→結果など、よく使う導線だけ軽くしたい
  • 「段階的にhtmxへ寄せる」:一気にSPA化せず、少しずつ置き換えたい

① 既存SSRに後付け

既存のSSRページに hx-boost を付けるだけで、リンク/フォーム遷移を“準SPA”にできます。
このデモでは <main> だけ差し替え、ヘッダー/フッターはSSRのまま残します。

PHP

<?php
// 型を厳密に扱う
declare(strict_types=1);

// HTMLエスケープ関数を用意する
function h(string $s): string {
	// 特殊文字をエスケープして返す
	return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

// 表示するページ種別を受け取る(home / customers / settings)
$page = (string)($_GET['page'] ?? 'home');

// 検索キーワードを受け取る(顧客ページ用)
$q = (string)($_GET['q'] ?? '');

// 前後空白を除去する
$page = trim($page);

// 前後空白を除去する
$q = trim($q);

// 有効なページ一覧を定義する
$allowed = ['home', 'customers', 'settings'];

// 不正なpageならhomeに戻す
if (!in_array($page, $allowed, true)) $page = 'home';

// 顧客一覧(ダミー)を用意する
$customers = [
	['id' => 101, 'name' => '田中商事', 'plan' => 'STANDARD'],
	['id' => 102, 'name' => 'みかん工業', 'plan' => 'PRO'],
	['id' => 103, 'name' => '鈴木電機', 'plan' => 'STANDARD'],
	['id' => 104, 'name' => '佐藤物流', 'plan' => 'ENTERPRISE'],
];

// 検索後に表示する顧客一覧を入れる配列
$filtered = [];

// customersページなら検索フィルタする
if ($page === 'customers') {

	// 1件ずつ見る
	foreach ($customers as $c) {

		// 検索が空なら全部入れる
		if ($q === '') {
			$filtered[] = $c;
			continue;
		}

		// 名前に部分一致したら入れる
		if (mb_stripos((string)$c['name'], $q) !== false) {
			$filtered[] = $c;
			continue;
		}
	}
}

// タイトルを決める
$titleMap = [
	'home'      => 'ホーム',
	'customers' => '顧客一覧',
	'settings'  => '設定',
];

// 表示タイトルを決める
$pageTitle = $titleMap[$page] ?? 'ホーム';
?>
<!doctype html>
<html lang="ja">
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width,initial-scale=1">
	<title><?= h($pageTitle) ?> | hx-boost(既存SSRに後付け)</title>
</head>
<body>

	<header class="HEADER">
		<h1>デモ①:既存SSRに後付け(最小の準SPA化)</h1>
		<p class="HTMX-NOTE">ヘッダーはSSRのまま固定、<code><main></code> だけをboostして部分遷移にします。</p>
	</header>

	<!--
		ここがポイント
		- hx-boost="true":リンク/フォームをAJAX化
		- hx-target="#BOOST1_MAIN":差し替え先はmain自身
		- hx-select="#BOOST1_MAIN":レスポンスからmainだけ抜く
		- hx-swap="outerHTML":mainを丸ごと置換(入れ子事故を防ぐ)
	-->
	<main
		id="BOOST1_MAIN"
		hx-boost="true"
		hx-target="#BOOST1_MAIN"
		hx-select="#BOOST1_MAIN"
		hx-swap="outerHTML"
	>

		<nav class="NAV">
			<a class="BTN" href="/htmx/demo/_boost_ssr.php?page=home">ホーム</a>
			<a class="BTN" href="/htmx/demo/_boost_ssr.php?page=customers">顧客一覧</a>
			<a class="BTN" href="/htmx/demo/_boost_ssr.php?page=settings">設定</a>
		</nav>

		<section class="SECTION">

			<h2><?= h($pageTitle) ?></h2>

			<?php if ($page === 'home'): ?>
				<p>
					これは“普通のSSRページ”ですが、<code>hx-boost</code> によってリンク遷移が部分更新になります。<br>
					まずはこういう「後付け」から始めると、段階的に快適化できます。
				</p>
			<?php endif; ?>

			<?php if ($page === 'customers'): ?>
				<p>
					フォーム送信もboost対象です。検索(GET)してもページ全体は再読み込みされず、<code><main></code> だけが差し替わります。
				</p>

				<form method="get" class="FORM">
					<input type="hidden" name="page" value="customers">
					<label>
						検索
						<input type="text" name="q" value="<?= h($q) ?>" placeholder="会社名で検索">
					</label>
					<button class="BTN is-ok" type="submit">検索</button>
				</form>

				<table class="TABLE">
					<thead>
						<tr><th>ID</th><th>会社名</th><th>プラン</th></tr>
					</thead>
					<tbody>
						<?php foreach ($filtered as $c): ?>
							<tr>
								<td><?= h((string)$c['id']) ?></td>
								<td><?= h((string)$c['name']) ?></td>
								<td><span class="BADGE"><?= h((string)$c['plan']) ?></span></td>
							</tr>
						<?php endforeach; ?>
					</tbody>
				</table>
			<?php endif; ?>

			<?php if ($page === 'settings'): ?>
				<p>
					設定ページのような「フォーム中心の画面」でも、遷移体験をそのまま準SPAにできます。<br>
					(このデモでは内容はダミーです)
				</p>

				<ul class="HTMX-LIST">
					<li>通知:ON</li>
					<li>テーマ:LIGHT</li>
					<li>権限:ADMIN</li>
				</ul>
			<?php endif; ?>

		</section>

	</main>

	<footer class="FOOTER">
		<p class="HTMX-NOTE">フッターもSSRのまま固定です(mainだけ差し替え)。</p>
	</footer>

</body>
</html>

デモ

① 既存SSRに後付け

解説

HTMLでやっていること

  • <main>hx-boost="true" を付け、リンククリック/フォーム送信をAJAX化します。
  • hx-target="#BOOST1_MAIN" で、差し替え先を main自身 に指定します。
  • hx-select="#BOOST1_MAIN" で、レスポンスHTMLから main だけ抜き出して使います。
  • hx-swap="outerHTML"main を丸ごと置換し、入れ子崩れを防ぎます。
  • リンク/フォームは普通の href/method="get" のままなので、非JS環境でもSSRとして動きます。

PHPでやっていること

  • $_GET['page'] で表示ページ(home/customers/settings)を切り替えます。
  • 顧客ページでは $_GET['q'] を受け取り、配列データを部分一致でフィルタします。
  • SSRとしてページ全体(ヘッダー/フッター含む)を返しますが、htmx側が main だけ抽出して反映します。

② 遷移を速く(一覧→詳細を部分更新)

よく使う導線(一覧→詳細)だけを部分更新にすると、体感速度が一気に上がります。
このデモでは「一覧はそのまま」「右の詳細パネルだけ差し替え」でサクサク遷移します。

PHP

<?php
// 型を厳密に扱う
declare(strict_types=1);

// HTMLエスケープ関数を用意する
function h(string $s): string {
	// 特殊文字をエスケープして返す
	return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

// 検索キーワードを受け取る
$q = (string)($_GET['q'] ?? '');

// 詳細IDを受け取る
$id = (string)($_GET['id'] ?? '');

// 前後空白を除去する
$q = trim($q);

// 前後空白を除去する
$id = trim($id);

// ダミーデータ(申請一覧)を用意する
$rows = [
	['id' => '801', 'title' => '申請:端末貸与', 'owner' => 'tanaka', 'status' => 'OPEN'],
	['id' => '802', 'title' => '申請:権限変更', 'owner' => 'sato',   'status' => 'PENDING'],
	['id' => '803', 'title' => '申請:アカウント追加', 'owner' => 'suzuki', 'status' => 'DONE'],
	['id' => '804', 'title' => '申請:VPN追加', 'owner' => 'tanaka', 'status' => 'OPEN'],
];

// 表示用の一覧を入れる配列
$list = [];

// 1件ずつ見る
foreach ($rows as $r) {

	// 検索が空なら全部入れる
	if ($q === '') {
		$list[] = $r;
		continue;
	}

	// titleに部分一致したら入れる
	if (mb_stripos((string)$r['title'], $q) !== false) {
		$list[] = $r;
		continue;
	}
}

// 選択中の詳細を初期化する
$detail = null;

// idが指定されていれば探す
if ($id !== '') {

	// 1件ずつ探す
	foreach ($rows as $r) {

		// 一致したら採用する
		if ((string)$r['id'] === $id) {
			$detail = $r;
			break;
		}
	}
}
?>
<!doctype html>
<html lang="ja">
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width,initial-scale=1">
	<title>遷移を速く(一覧→詳細) | hx-boost</title>
</head>
<body>

	<header class="HEADER">
		<h1>デモ②:遷移を速く(一覧→詳細を部分更新)</h1>
		<p class="HTMX-NOTE">一覧は残し、右の詳細パネルだけを入れ替えます。</p>
	</header>

	<div class="GRID-2COL">

		<!-- 左:一覧(検索・リンクはboost) -->
		<section
			id="BOOST2_LIST"
			class="SECTION"
			hx-boost="true"
			hx-target="#BOOST2_LIST"
			hx-select="#BOOST2_LIST"
			hx-swap="outerHTML"
		>
			<h2>申請一覧</h2>

			<form method="get" class="FORM">
				<label>
					検索
					<input type="text" name="q" value="<?= h($q) ?>" placeholder="タイトルで検索">
				</label>
				<button class="BTN is-ok" type="submit">検索</button>
			</form>

			<table class="TABLE">
				<thead>
					<tr><th>ID</th><th>タイトル</th><th>担当</th><th>ステータス</th></tr>
				</thead>
				<tbody>
					<?php foreach ($list as $r): ?>
						<tr>
							<td><?= h((string)$r['id']) ?></td>

							<!--
								ここがポイント
								- 一覧はboostで部分更新(検索など)
								- “詳細リンクだけ”は右パネルをtargetにする
							-->
							<td>
								<a
									href="/htmx/demo/_boost_fast.php?q=<?= h($q) ?>&id=<?= h((string)$r['id']) ?>"
									hx-target="#BOOST2_DETAIL"
									hx-select="#BOOST2_DETAIL"
									hx-swap="outerHTML"
								><?= h((string)$r['title']) ?></a>
							</td>

							<td><?= h((string)$r['owner']) ?></td>
							<td><span class="BADGE"><?= h((string)$r['status']) ?></span></td>
						</tr>
					<?php endforeach; ?>
				</tbody>
			</table>

		</section>

		<!-- 右:詳細パネル(ここだけ差し替えられる) -->
		<section id="BOOST2_DETAIL" class="SECTION">
			<h2>詳細</h2>

			<?php if ($detail === null): ?>
				<p class="HTMX-NOTE">左の一覧から1件選ぶと、このパネルだけ更新されます。</p>
			<?php else: ?>
				<ul class="HTMX-LIST">
					<li><strong>ID:</strong><?= h((string)$detail['id']) ?></li>
					<li><strong>タイトル:</strong><?= h((string)$detail['title']) ?></li>
					<li><strong>担当:</strong><?= h((string)$detail['owner']) ?></li>
					<li><strong>ステータス:</strong><?= h((string)$detail['status']) ?></li>
				</ul>
			<?php endif; ?>

		</section>

	</div>

</body>
</html>

デモ

② 遷移を速く(一覧→詳細を部分更新)

別タブで開く

解説

HTMLでやっていること

  • 一覧エリア(左カラム)に hx-boost を付け、検索フォームの遷移を部分更新にします。
  • 詳細リンクは hx-target="#BOOST2_DETAIL" を指定し、右の詳細パネルだけを差し替えます。
  • 詳細表示も hx-select="#BOOST2_DETAIL" で必要部分だけ抜き出し、hx-swap="outerHTML" で丸ごと置換します。
  • “全部の遷移”をboostするのではなく、“速くしたい導線だけ”ターゲットを分けるのがコツです。

PHPでやっていること

  • ダミーの申請一覧配列を用意し、$_GET['q'] でタイトル検索を行います。
  • $_GET['id'] が指定されていれば、その行の詳細データを探して右パネルに表示します。
  • 返しているのは普通のSSRページですが、htmxが「左だけ」「右だけ」を選んで差し替えます。

③ 段階的にhtmxへ寄せる

いきなり全画面を“準SPA化”しなくてもOKです。
まずは「このエリアだけ」boostし、徐々にhtmxに寄せる移行ができます。

PHP

<?php
// 型を厳密に扱う
declare(strict_types=1);

// HTMLエスケープ関数を用意する
function h(string $s): string {
	// 特殊文字をエスケープして返す
	return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

// タブ種別を受け取る(profile / billing / export)
$tab = (string)($_GET['tab'] ?? 'profile');

// 前後空白を除去する
$tab = trim($tab);

// タブの許可リストを用意する
$allowed = ['profile', 'billing', 'export'];

// 不正なtabならprofileに戻す
if (!in_array($tab, $allowed, true)) $tab = 'profile';

// タブタイトルを用意する
$tabTitleMap = [
	'profile' => 'プロフィール',
	'billing' => '請求設定',
	'export'  => 'エクスポート',
];

// 表示タイトルを決める
$tabTitle = $tabTitleMap[$tab] ?? 'プロフィール';
?>
<!doctype html>
<html lang="ja">
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width,initial-scale=1">
	<title>段階的にhtmxへ寄せる | hx-boost</title>
</head>
<body>

	<header class="HEADER">
		<h1>デモ③:段階的にhtmxへ寄せる(boost範囲を小さく始める)</h1>
		<p class="HTMX-NOTE">
			「この画面だけ速くしたい」など、狭い範囲から始められます。<br>
			(このデモでは“中央の設定エリア”だけboostしています)
		</p>
	</header>

	<!-- ここはSSRのまま(boostしない) -->
	<nav class="NAV">
		<a class="BTN" href="/htmx/demo/_boost_ssr.php?page=home">(SSR)ホームへ</a>
		<a class="BTN" href="/htmx/demo/_boost_ssr.php?page=customers">(SSR)顧客へ</a>
	</nav>

	<!--
		ここだけをboostして「部分遷移」を段階導入する
		- hx-select/hx-target/hx-swap で #BOOST3_MAIN を丸ごと差し替え
	-->
	<main
		id="BOOST3_MAIN"
		class="SECTION"
		hx-boost="true"
		hx-target="#BOOST3_MAIN"
		hx-select="#BOOST3_MAIN"
		hx-swap="outerHTML"
	>
		<h2><?= h($tabTitle) ?></h2>

		<nav class="NAV">
			<a class="BTN" href="/htmx/demo/_boost_step.php?tab=profile">プロフィール</a>
			<a class="BTN" href="/htmx/demo/_boost_step.php?tab=billing">請求設定</a>
			<a class="BTN" href="/htmx/demo/_boost_step.php?tab=export">エクスポート</a>
		</nav>

		<?php if ($tab === 'profile'): ?>
			<p>ここは設定エリアだけ部分遷移します。既存SSRに“後付け”しやすい導入形です。</p>
			<ul class="HTMX-LIST">
				<li>表示名:みかん</li>
				<li>メール:mikan@example.com</li>
			</ul>
		<?php endif; ?>

		<?php if ($tab === 'billing'): ?>
			<p>タブ切り替えはリンク遷移ですが、<code>hx-boost</code> により準SPAのように切り替わります。</p>
			<ul class="HTMX-LIST">
				<li>支払い方法:クレジットカード</li>
				<li>請求日:毎月末</li>
			</ul>
		<?php endif; ?>

		<?php if ($tab === 'export'): ?>
			<p>
				“全リンクをboostしたくない”もの(ファイルDLなど)は、リンク単体で <code>hx-boost="false"</code> にできます。
			</p>

			<ul class="HTMX-LIST">
				<li>
					<a class="BTN" href="/htmx/demo/_dummy_export.csv" hx-boost="false">CSVダウンロード(SSRのまま)</a>
				</li>
				<li class="HTMX-NOTE">※このリンクはboostしない例です(実ファイルは任意で用意)</li>
			</ul>
		<?php endif; ?>

	</main>

</body>
</html>

デモ

③ 段階的にhtmxへ寄せる

別タブで開く

解説

HTMLでやっていること

  • #BOOST3_MAIN(設定エリア)だけに hx-boost を付け、タブ切り替えを部分遷移化します。
  • hx-target="#BOOST3_MAIN"hx-select="#BOOST3_MAIN" で、中央エリアだけ差し替える構成にします。
  • hx-swap="outerHTML" でエリアを丸ごと置換し、内部DOMの崩れを防ぎます。
  • boostしたくないリンク(例:ファイルDL)は hx-boost="false" で除外できます。

PHPでやっていること

  • $_GET['tab'] を受け取り、profile/billing/export の表示を切り替えます。
  • タブ内容はSSRで生成しますが、差し替えは htmx が担当するため準SPAのように見えます。
  • 導入初期は「一部エリアだけ boost」→慣れたら範囲拡大、という移行がしやすいです。

このページの著者

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

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

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

得意:PHP・JavaScript・MySQL・CSS

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

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

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

もちもちみかん.comとは


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

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