htmx 4.0 がリリースされました
2026 年 8 月 28 日に htmx 4.0.0 がリリースされました。
htmx は、HTML に hx-get や hx-post を書くだけで、サーバーから返ってきた HTML を画面へ差し込めるライブラリです。個人的には「クライアント側へ大きなフレームワークを持ち込みたくないけど、ページ全体を毎回再読み込みするのも避けたい」という場面でちょうどいい存在だと思っています。
今回の 4.0 は、普段の書き方が別物になるようなアップデートではありません。hx-get や hx-target など、おなじみの部分はほぼそのままです。
一方で、属性の継承やイベント名などが整理され、Morph Swap や複数箇所更新の仕組みも強化されました。実際に触ってみて、気になったところをまとめます。
暗黙動作を減らし、HTML を局所的に理解しやすくした
属性継承を :inherited で明示する。
htmx 2 では、親要素に書いた一部の属性が子要素へ暗黙的に継承されていました。htmx 4 では、継承させたい属性へ :inherited を付けます。
<article
hx-target:inherited="#result"
hx-swap:inherited="innerHTML">
<button hx-get="/message">
メッセージを取得
</button>
<div id="result"></div>
</article>
ボタン自身には hx-target がありませんが、親の hx-target:inherited が適用されるので、レスポンスは #result へ入ります。
最初は少し記述が増えたように感じました。ただ、子要素だけ見て「この hx-target はどこから来たんだろう」と探し回ることが減るので、長期的にはこちらのほうが読みやすそうです。
htmx 2 から移行する場合は、ここが一番影響の大きい変更だと思います。
Morph Swap でサーバーレンダリング UI の操作感を改善した
htmx 4 では、Idiomorph ベースの Morph Swap がコアへ入りました。
使い方は hx-swap へ innerMorph または outerMorph を指定するだけです。
<button
hx-get="/profile/edit"
hx-target="#profile"
hx-swap="innerMorph">
更新
</button>
<div id="profile">
<label for="display-name">表示名</label>
<input id="display-name" name="display_name">
</div>
通常の innerHTML はターゲット内を置き換えます。一方、innerMorph は既存の DOM をレスポンスの形へ変形します。
同じ ID を持つ要素をできるだけ維持してくれるので、入力値やフォーカス、スクロール位置などを保ちやすいのがポイントです。実際に入力欄へ文字を入れた状態で更新してみると、入力値を残したまま周辺の HTML だけ変えられました。
何でも Morph にすればよいわけではありませんが、フォームや動画など、DOM 要素の状態を維持したい画面では便利そうです。
<hx-partial> で複数箇所更新を明確に表現できるようになった
今回「一番使ってくことになるだろうな。」と感じたのが <hx-partial> です。例えば、フォームを送信したあとに次の 3 箇所を同時に更新したいとします。
- メッセージ一覧へ新しい項目を追加する
- 通知件数を更新する
- メイン領域へ結果を表示する
呼び出し側は普通の htmx です。
<button
hx-get="/dashboard/update"
hx-target="#main-result">
更新
</button>
<div id="message-feed"></div>
<div id="notification-count">0</div>
<div id="main-result"></div>
サーバーは次のような HTML を返します。
<hx-partial hx-target="#message-feed" hx-swap="append">
<p>新しいメッセージが届きました</p>
</hx-partial>
<hx-partial hx-target="#notification-count">
<strong>3</strong>
</hx-partial>
<p>ダッシュボードを更新しました</p>
これで、1 つ目の <hx-partial> は #message-feed へ追記され、2 つ目は #notification-count を置き換えます。残った通常の HTML は、ボタンで指定した #main-result へ入ります。従来の hx-swap-oob に近い仕組みですが、<hx-partial> はレスポンス側だけを見ても、どこへ、どの方法で差し込むのかが分かります。
<hx-partial
hx-target="#notification-count"
hx-swap="innerHTML">
...
</hx-partial>
hx-swap-oob も引き続き使えます。単純な同一 ID 要素の置換なら OOB、更新先や swap 方法を分かりやすく書きたいなら <hx-partial>、という使い分けになりそうです。
hx-push-url 後のページ復元方法が変わった
hx-push-url や hx-replace-url は htmx 4 でも引き続き使えます。変わったのは、そこで作られた履歴をブラウザの「戻る」「進む」で復元する方法です。
<a hx-get="/articles" hx-push-url="true" hx-target="#main">
記事一覧
</a>
htmx 2 は、遷移前の DOM を localStorage へスナップショットとして保存し、戻る操作でそれを復元していました。htmx 4 は DOM を保存せず、戻り先の URL へ GET リクエストを送り直します。レスポンスはデフォルトで <body> へ、hx-history-elt があればその要素へ swap されます。
つまり、hx-push-url がなくなったのではなく、「履歴へ URL を積むところは同じ、戻るときはキャッシュした DOM ではなくサーバーから取り直す」という変更です。
第三者の JavaScript が書き換えた古い DOM や、ユーザー固有の HTML がブラウザストレージに残る問題を減らせます。その代わり、履歴へ積んだ URL へ直接アクセスしても完全なページを返せるようにしておく必要があります。
ローカルキャッシュが必要なら、hx-history-cache 拡張で sessionStorage ベースの復元を選べます。
Alpine.js と組み合わせるための公式拡張が追加
htmx と Alpine.js を一緒に使っている人には、公式の hx-alpine-compat 拡張が追加されたのも気になるところです。読み込み順は、htmx、互換拡張、Alpine.js です。
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/htmx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/ext/hx-alpine-compat.js"></script>
<script defer src="/js/alpine.js"></script>
Alpine.js は hx-alpine-compat より後に読み込みます。htmx 4 では、拡張スクリプトを読み込めばページ全体へ適用されるため、hx-ext 属性は必要ありません。この拡張は、htmx の swap が完了する前に Alpine.js が更新途中の DOM を初期化してしまう問題を避けます。htmx の settle が完了して DOM が安定してから、Alpine.js の初期化を進めてくれます。
特に効果が分かりやすいのは Morph Swap です。
<div
id="profile"
x-data="{ editing: false }"
hx-get="/profile"
hx-swap="outerMorph">
<button x-on:click="editing = !editing">
編集
</button>
</div>
hx-alpine-compat は Morph 前に Alpine.js のデータスタックを新しいノードへ引き継ぎ、editing のようなローカル状態が失われにくいようにします。Alpine.js が :id や x-bind:id で動的に ID を付ける場合のノード照合にも対応します。また、htmx 4 は履歴復元時に DOM をサーバーから取り直すため、古い DOM と Alpine.js の内部状態が食い違う問題も起きにくくなりました。hx-history-cache 拡張を使う場合は、hx-alpine-compat が Alpine.js の状態保存と復元も補助します。
単純に別々の領域で使うだけなら、互換拡張なしでも問題にならないことがあります。ただし、htmx が Alpine.js コンポーネントを含む HTML を swap する場合や、innerMorph、outerMorph、hx-history-cache を使う場合は、最初から読み込んでおくのがよさそうです。
まとめ
htmx 4 を触ってみて、基本的な使い心地は htmx 2 のままだと感じました。hx-get で HTML を取りに行き、hx-target へ差し込むという中心部分は変わっていません。しかし、これまでは裏側では XMLHttpRequest を使っていましたが、htmx 4 では fetch() へ移行していたりもします。
そのうえで、今回よかったと感じたのは次の 4 つです。
:inheritedで属性継承の意図が見えるようになった- Morph Swap が組み込まれ、入力状態などを維持しやすくなった
<hx-partial>で複数箇所更新を分かりやすく書けるようになったfetch()を基盤に、ストリーミング HTML へ進みやすくなった
特に <hx-partial> は、サーバーが返す HTML 自身に「どこへ、どう入るか」を書けるので、htmx の考え方とよく合っています。ストリーミング HTML と組み合わせたときにどう使えるのか、もう少し試してみたいところです。
既存の htmx 2 をすぐ全部移行する必要はなさそうですが、属性継承、イベント、エラーレスポンス、独自拡張を確認できるなら、htmx 4 へ移る価値は十分ありそうです。