htmx(エイチティーエムエックス)は、HTMLにhx-getやhx-postなどの属性を書くだけで、ボタンや入力欄からサーバーへリクエストを送り、返ってきたHTMLで画面の一部を差し替えられるJavaScriptライブラリです。この記事では、仕組みと現行バージョン(npmの既定は2.0.11、4.0.0は別タグ)を押さえたうえで、採用前に確かめておきたい「デメリット」を公式資料に沿って具体的に整理し、XSS・CSRFの設定、Flaskで動かした最小構成、ReactやAlpine.jsとの選び方までを扱います。
まとめ:htmxはサーバーが返すHTMLで画面を部分更新するライブラリ
htmxは、JSONを受け取ってブラウザ側で画面を組み立てるSPAとは逆に、サーバーが表示用のHTML断片を返し、ブラウザはそれを指定の場所へ差し込むだけにする設計です。依存ライブラリは無く、2026年11月時点でnpmのlatestは2.0.11(公式トップの表記は約16k min.gz)です。2026年8月28日に4.0.0が出ましたが、npmではnextタグに置かれ、latestへの切り替えは2027年初頭の予定です。
向いているのは、フォーム送信、一覧の追記や絞り込み、インライン編集など、更新が画面の決まった区画に収まる業務画面です。向かないのは、表計算や地図のように多数の状態が互いに依存し、マウス操作のたびに画面が変わるUIと、オフライン動作が必須のアプリです。導入するなら、ユーザー入力のエスケープ、hx-headersによるCSRFトークンの送信、Vary: HX-Requestの付与の3点を最初に決めておきます。
htmxの仕組み:hx-*属性によるリクエスト送信とHTML断片の差し替え
htmxの動作は、イベント、リクエスト、差し替えの3段で説明できます。hx-triggerで指定したイベント(既定はボタンならclick、フォームならsubmit)が起きると、hx-get・hx-postなどに書いたURLへリクエストが飛びます。サーバーはHTML断片を返し、htmxはhx-targetで選んだ要素へ、hx-swapの方式(既定はinnerHTML)で差し込みます。ページ全体は再読み込みされません。
<button hx-get="/contacts/1/edit" hx-target="#contact" hx-swap="outerHTML">
編集する
</button>
<div id="contact">山田太郎 / [email protected]</div>
このボタンを押すとGET /contacts/1/editが送られ、サーバーが返した編集フォームのHTMLが#contactと置き換わります。fetchもイベントリスナーも書いていません。htmxが送るリクエストにはHX-Request: trueヘッダーが付くため、サーバーはこれを見て「ページ全体」と「断片」を返し分けます。
出発点はリンクとフォームへの4つの問い
公式トップは、htmxの動機を4つの問いで示しています。なぜ<a>と<form>だけがHTTPリクエストを送れるのか、なぜclickとsubmitだけが送信のきっかけなのか、なぜGETとPOSTだけなのか、なぜ画面全体しか置き換えられないのか、です。htmxはこの制約を外し、任意の要素・任意のイベント・PUTやDELETEを含む任意のメソッド・任意の区画で同じことをできるようにしたものです。前身は、作者のCarson Grossが手がけたjQuery前提のintercooler.jsで、htmxはjQuery依存を外して作り直されました。
htmxでできることと拡張で補う機能
コアだけで、Ajaxによる部分更新、CSSトランジション、hx-boost(リンクとフォームをまとめてAjax化し、既定でbodyを差し替える)、hx-push-urlによる履歴操作、hx-indicatorによる読み込み中表示までを扱えます。WebSocketとServer-Sent Eventsは拡張で提供され、2.0からは拡張がコアの配布物から切り離されて個別に配布されています。属性ごとの書き方と実装パターンはhtmxの使い方:hx-属性の実装手順からFastAPI連携まで解説で手順として解説しています。
| 属性 | 役割 | 既定値・例 |
|---|---|---|
| hx-get / hx-post | リクエストの送信先 | hx-put・hx-patch・hx-deleteもある |
| hx-trigger | 送信のきっかけ | input changed delay:300ms |
| hx-target | 差し替える要素 | 既定は自分自身 |
| hx-swap | 差し込み方 | 既定はinnerHTML |
| hx-headers | 追加ヘッダー | CSRFトークンの送信 |
htmxの現行バージョン:npmのlatestは2.0.11、4.0.0はnextタグ
htmxは2系と4系が並行しています(3系はありません)。npmレジストリを2026年11月1日に確認した時点で、配布タグはlatestが2.0.11、nextが4.0.0でした。npm install htmx.orgで入るのは2.0.11です。配布サイズは、jsDelivrから取得したdist/htmx.min.jsをgzip -9c < htmx.min.js | wc -cのように標準入力から圧縮して測りました(gzipヘッダーにファイル名を含まない値)。
| 系列 | 最新版 | npm公開日 | npmタグ | min.js | gzip後 |
|---|---|---|---|---|---|
| 2系 | 2.0.11 | 2026年9月22日 | latest | 52,182バイト | 16,873バイト |
| 4系 | 4.0.0 | 2026年8月28日 | next | 36,716バイト | 13,079バイト |
2.0.0は2024年6月17日の公開で、1系からIE対応を外し、selfRequestsOnly(同一ドメインへのリクエストだけを許可する設定)の既定値をtrueに変えました。公式トップはIEが必要なら1系を使うよう案内しており、1系は期限を設けずに保守するとしています。解説記事でよく見る「約14KB」はGitHubのREADMEに残る表記で、2.0.11の実測値は上の表のとおり約16.5KiBと、公式トップの約16kの方が実態に近い値です。
4.0.0は通信基盤をXMLHttpRequestからfetch()へ移し、属性の継承を既定で無効にする(継承させたい属性に:inheritedを付ける)など、2系のテンプレートがそのままでは動かない変更を含みます。公式のリリース告知は、2027年初頭まで2系をlatestに据え置くと説明しており、作者は2系も期限なしでサポートすると表明しています。破壊的変更の一覧と移行の手順はhtmx 4.0とは:2系からの変更点と移行手順をコードで解説にまとめました。
htmxのデメリット:向かない画面と、導入後にチームが背負うもの
htmxの弱点は「HTMLの属性で書ける範囲が狭い」という一言では済みません。公式エッセイWhen Should You Use Hypermedia?は、ハイパーメディア(サーバーが返すHTML)が向かない条件を5つ挙げており、作者自身が批判を集めたhtmx sucks(2024年2月1日)も、コンポーネント資産・採用・API二重化を論点にしています。ここでは、採否を分ける順に並べます。
状態の相互依存が多い画面とオフライン動作
公式が最初に挙げるのは、画面の広い範囲に動的な依存関係があるUIです。例に挙がっているのはGoogle Sheetsで、1つのセルを書き換えるたびに多数のセルが再計算されるため、そのつどサーバーへ問い合わせる設計は成り立ちません。同じくGoogle Mapsのように、マウスの動きに合わせて状態が絶えず変わるUIも、往復の遅延が操作感を壊します。オフラインで完全に動く必要があるアプリも、サーバーがHTMLを返す前提と両立しません。こうした画面が製品の中心なら、htmxを主軸にすべきではありません。
操作ごとのサーバー往復と、断片を返すエンドポイントの増加
htmxでは画面を変える操作の多くがHTTPリクエストになります。開閉メニューやタブ切り替えのようにサーバーのデータを必要としない動きまでhtmxで書くと、無駄な往復と遅延が増えます。この種の動きはAlpine.jsや素のJavaScript、HTMLの<details>要素に任せるのが定石です。また、区画ごとに専用ルートを設ける設計では、エンドポイントの数が増えます。同じURLでページ全体と断片を返し分ける設計も可能です。スマートフォンアプリや外部連携のためにJSON APIも必要な場合、HTML用とJSON用の2系統を保守することになります(htmx sucksでは「Duplicating Your APIs」として取り上げられています)。
コンポーネント資産・型・採用市場の薄さ
Reactには、日付選択や高機能なテーブルなどの部品と、ShadCNのようにコードを写して使うコンポーネント集が大量にあります。公式エッセイも、こうしたコピー&ペースト型の部品を使いたいならハイパーメディアは向かないと明記しています。htmx本体はJavaScriptで書かれており(型定義ファイルは同梱)、テンプレートに書いたhx-属性や値の誤記は、TypeScriptの型検査でもビルドでも見つかりません。さらに「チームが乗り気でない」ことも、公式が技術以外の不向きとして挙げる条件です。htmxの経験者は求人市場でReactより少ないため、外注や増員の計画がある案件ほど、この点を先に確認します。
キャッシュとエラー応答の落とし穴
同じURLで「ページ全体」と「断片」を返し分けると、ブラウザやCDNのキャッシュに両者が混ざり、戻るボタンで断片だけの画面が出ることがあります。公式ドキュメントのCaching節は、HX-Requestの有無で返す内容を変えるならVary: HX-Requestを付けるよう求めています。2系でこの返し分けを行う場合は、htmx.config.historyRestoreAsHxRequest = falseも設定し、履歴復元時にはページ全体を返すようにします。もう1つは、2系が4xx・5xxの応答を既定で画面に差し替えない点です。入力エラーを422で返してエラー表示用のHTMLを出したい場合は、htmx.config.responseHandlingで差し替えを許可する設定が要ります。4.0ではこの既定が逆になり、204と304以外の応答がすべて差し替わります。
htmxのセキュリティ:XSS・CSRF・CSPで決めておく設定
htmxは「マークアップに振る舞いを書く」ライブラリなので、HTMLに紛れ込んだ属性がそのまま通信を起こします。公式ドキュメントのSecurity節の第1ルールが「ユーザー由来のコンテンツはすべてエスケープする」であるのはこのためです。
XSS対策:エスケープとhx-disableによる属性処理の抑止
投稿文やプロフィールなどをHTMLとして表示する箇所で、利用者がhx-getやhx-trigger="load"を含むタグを書き込めると、閲覧者のブラウザから意図しないリクエストが送られます。テンプレートエンジンの自動エスケープを切らないことが前提で、どうしても生のHTMLを表示する場合は、許可するタグと属性をホワイトリストで絞ったサニタイザーを通します。そのうえで、利用者のコンテンツを表示する要素をhx-disableで囲むと、その要素と子孫ではhtmxの処理が行われません。親のどこかにhx-disableがあれば、注入された内容の側から無効化を解除することはできません。なお4.0ではこの役割の属性がhx-ignoreに改名され、hx-disableは通信中に要素を無効化する別の意味になるため、移行時は置換の順序に注意します。
CSRF対策:hx-headersによるトークン送信と属性継承
Cookieでログイン状態を持つアプリでは、hx-postやhx-deleteもCSRF対策の対象です。公式が示す方法は、<html>か<body>にhx-headersでトークンを書き、子孫のすべてのリクエストへ継承させる書き方です。
<body hx-headers='{"X-CSRF-Token": "{{ csrf_token }}"}'>
注意点は2つあります。hx-boostで画面を遷移させるとbodyの属性は置き換わらないため、トークンを更新する設計なら差し替えられる要素側に書きます。また4.0では属性の継承が明示制になったため、同じ書き方では子要素へ効かず、hx-headers:inheritedと書く必要があります。付け忘れてもエラーは出ず、サーバー側で403が返って初めて気づく種類の不具合です。
selfRequestsOnly・allowScriptTags・allowEvalとCSP
2系には、被害の範囲を狭める設定が用意されています。selfRequestsOnlyは2.0から既定でtrueで、別ドメインへのリクエストを拒否します。allowScriptTagsは既定でtrueで、falseにすると差し込んだHTML内の<script>を実行しません。allowEvalをfalseにすると、hx-on:やイベントフィルターのようなevalに依存する機能が止まり、公式APIリファレンスはこれをCSP(Content Security Policy)との互換のための設定と説明しています。unsafe-evalを許さないCSPを敷くサイトでは、allowEval: falseを前提にテンプレートを書きます。機密情報を表示するページはhx-history="false"にして、localStorageの履歴キャッシュへ保存させないようにします。
Flaskで動かすhtmxの最小構成:断片の返却とCSRF検証
サーバー側でやることは、断片を返すルートを用意し、トークンを検証し、Varyを付けることです。次のコードはFlask 3.1.3で、一覧に項目を追記する画面を作った例です。仮想環境でpython -m pip install Flask==3.1.3を実行し、コードをapp.pyに保存してflask --app app runで起動します。
from flask import Flask, request, render_template_string, abort
import secrets
app = Flask(__name__)
CSRF = secrets.token_urlsafe(16)
ITEMS = ["htmx", "Flask"]
PAGE = """<!doctype html>
<html lang="ja">
<head>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/htmx.min.js"></script>
</head>
<body hx-headers='{"X-CSRF-Token": "{{ csrf }}"}'>
<form hx-post="/items" hx-target="#list" hx-swap="beforeend">
<input name="name" required>
<button>追加</button>
</form>
<ul id="list">{% for i in items %}<li>{{ i }}</li>{% endfor %}</ul>
</body>
</html>"""
@app.get("/")
def index():
return render_template_string(PAGE, items=ITEMS, csrf=CSRF)
@app.post("/items")
def add_item():
if request.headers.get("X-CSRF-Token") != CSRF:
abort(403)
name = request.form["name"]
ITEMS.append(name)
resp = app.make_response(render_template_string("<li>{{ n }}</li>", n=name))
resp.headers["Vary"] = "HX-Request"
return resp
Flaskのテストクライアントで、HX-Request: trueとトークンを付けて<img src=x onerror=alert(1)>という名前を送ると、応答は<li><img src=x onerror=alert(1)></li>とエスケープされた断片になり、Vary: HX-Requestが付きました。トークンを外して送った場合は403です。Jinja2の{{ }}は自動でエスケープされるので、|safeやMarkupを使う箇所には特に注意が必要です。自動エスケープが有効でも、引用符のない属性への値の埋め込みや、href属性に入るURLのスキームなどは別途検証します。本番では、トークンは利用者のセッションごとに発行します(Flask-WTFのCSRFProtectなどを使います)。FastAPIとJinja2での実装は前掲の使い方記事に、Laravelで断片を返す@fragmentはLaravel Blade Fragmentsの使い方|@fragmentとhtmx・Turbo連携の落とし穴に分けて書いています。
htmxとReact・Alpine.js・Hotwireの違いと選び方
近い技術との違いは、画面の状態をどこに置くかで整理できます。
| 技術 | 状態の置き場所 | サーバーの応答 | 向く画面 |
|---|---|---|---|
| htmx | サーバー | HTML断片 | CRUD・一覧・フォーム |
| React(クライアント描画の例) | ブラウザ | JSONなど | 状態の多い対話型UI |
| Alpine.js | ブラウザ(要素単位) | 規定なし(fetch等で通信可能) | 開閉・タブ・入力補助 |
| Hotwire(Turbo) | サーバー | HTML/Turbo Streams | Railsの業務画面 |
Alpine.jsは競合ではなく補完関係で、通信を伴わない開閉や選択状態をAlpine.js、サーバーのデータが要る更新をhtmxに分ける組み合わせがよく使われます(Alpine.jsとは?18ディレクティブの実装とCSP対応・採用判断)。Railsなら標準で組み込まれるHotwire(Turbo・Stimulus)が先に候補になり、DjangoやFlask、Go、PHPのようにフロントエンドの標準が決まっていない環境でhtmxの手軽さが生きます。
判断の基準は1つで、「画面の状態をサーバーが返すHTMLで表せるか」です。表せるなら、htmxで書いた方がビルド構成もAPI設計も少なく済みます。地図や表計算のような画面が一部にだけあるなら、サイトの大半をhtmxで作り、その部分だけReactを埋め込む構成が取れます。逆に、画面の大半がクライアントの状態で動く製品にhtmxを当てると、結局JavaScriptを書き足すことになり、どちらの利点も得られません。業務システムのSPA化を検討する際の判断材料はSPAの事例|Gmail・GitHub・freeeに学ぶ業務システムのSPA化の判断基準に、既存のサーバーサイドアプリへhtmxを組み込む開発の相談は業務用・Webアプリ開発で受け付けています。
よくある質問
htmxのデメリットは何ですか?
多数の状態が相互に依存する画面(表計算・地図など)、マウス操作に即応する画面、オフライン動作には向きません。運用面では、断片を返すエンドポイントが増えること、JSON APIと二重になりうること、Reactほど部品と経験者が多くないことが挙げられます。2系は4xx・5xxの応答を既定で差し替えない点も、エラー表示の設計で注意が要ります。
htmxに対応しているブラウザは?
2系はInternet Explorerに対応していません。公式はIEが必要な場合に1系を使うよう案内しており、1系は期限を設けずに保守されます。最低バージョンを並べた公式の対応表は公開されていないため、IE以外の現行ブラウザを前提に、対象ブラウザで動作を確認してから採用します。
npm install htmx.orgで入るのは2系と4系のどちらですか?
2026年11月時点では2系の2.0.11です。4.0.0はnextタグなので、npm install htmx.org@nextか[email protected]のように明示した場合だけ入ります。公式は2027年初頭に4系をlatestへ切り替える予定としているため、本番ではバージョンを固定しておきます。
htmxでCSRF対策はどうすればよいですか?
<body>などにhx-headersでCSRFトークンを書き、子孫のすべてのリクエストに付けます。サーバーは通常のフォームと同じ検証をヘッダーに対して行います。4.0ではhx-headers:inheritedと書かないと子要素に継承されません。
htmxとFlaskはどう連携しますか?
Flaskのルートから、ページ全体ではなく差し替える部分だけのHTMLを返します。同じURLでページ全体と断片を返し分ける場合は、request.headers.get("HX-Request")で判別し、応答にVary: HX-Requestを付けます。追加のパッケージは必須ではありません。