はじめに
正直なところ、最近のフロントエンド開発は複雑すぎると思いませんか。
React、Vue、Angular...どれを選んでも、ビルド設定、状態管理、ルーティングと覚えることが山ほどある。30代になって思うのは、シンプルに動くものが一番だということ。
そんな「JavaScriptフレームワーク疲れ」を感じている人に朗報です。htmxというライブラリを使えば、HTML属性を書くだけでモダンなWebアプリが作れます。
htmxはGitHubで46,700スター以上を獲得している、いま注目のライブラリ。「HTMLを完成させる」という哲学のもと、JavaScriptをほとんど書かずにAJAX通信やリアルタイム更新を実現できます。
これ、意外と革命的なんですよ。
とはいえ、百聞は一見にしかず。hx-getやhx-swapといった属性を実際に書き換えながら、htmxがブラウザ上でどうDOMを差し替えるのか動かして確認できるサンプルを用意しました。先に挙動を見たい方はこちらからどうぞ。
htmxとは
htmxは、HTML属性を通じてAJAX、WebSocket、Server Sent Eventsに直接アクセスできる軽量ライブラリです。バージョン2.0.8(2025年12月時点)がリリースされており、444人のコントリビューターによって活発に開発されています。
最大の特徴はJavaScript不要という点。HTMLの属性を追加するだけで、従来はJavaScriptで書いていた動的な処理が実現できます。
<button hx-post="/clicked" hx-swap="outerHTML">
Click Me
</button>
これだけで、ボタンをクリックしたときにサーバーにPOSTリクエストを送り、レスポンスでボタン自体を置き換えることができます。jQueryすら不要です。
公式サイト: https://htmx.org GitHub: https://github.com/bigskysoftware/htmx
特徴・メリット
1. 圧倒的な軽量さ
htmxは**約18kB(gzip圧縮時)**という軽さです。依存関係ゼロで、CDNから読み込むだけで使えます。
React + ReactDOM: 約45kB (gzip)
Vue 3: 約34kB (gzip)
htmx: 約18kB (gzip)
ReactやVueと比べて半分以下のサイズ。コスパ的にかなり優秀ですね。
2. 学習コストの低さ
新しいプログラミング言語や複雑なAPIを覚える必要がありません。HTMLを書ける人なら、すぐに使い始められます。
従来のJavaScriptで書くと:
document.getElementById('btn').addEventListener('click', async () => {
const response = await fetch('/api/data', { method: 'POST' });
const html = await response.text();
document.getElementById('result').innerHTML = html;
});
htmxなら:
<button hx-post="/api/data" hx-target="#result">
データを取得
</button>
<div id="result"></div>
個人的には、このシンプルさがたまらない。
3. HTMLの制約からの解放
従来のHTMLでは、HTTPリクエストを発行できるのは<a>タグと<form>タグだけでした。htmxはこの制約を取り払います。
- どんな要素からでもリクエスト可能
- GET/POST以外のHTTPメソッドも使える
- 任意のイベントでリクエストをトリガーできる
- レスポンスを任意の場所に挿入できる
「ハイパーテキストとしてのHTMLを完成させる」という設計哲学は、なかなか筋が通っています。
4. サーバーサイド言語を選ばない
htmxはサーバーからHTMLを返すだけでOK。Python、Ruby、PHP、Go、Node.js、何でも使えます。
SPAのようにJSONを返してフロントで組み立てる必要がない。これはバックエンドエンジニアにとって非常にありがたい。
5. 67%のコード削減実績
htmxの公式サイトによると、ReactからhtmxへのリプレイスでJavaScriptのコード量を67%削減した実例があるとのこと。
コードが減れば、バグも減る。メンテナンスも楽になる。QOL上がりますね。
インストール方法
CDNから読み込む(推奨)
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
これだけです。ビルド設定もバンドラーも不要。時短になりますね。
npmでインストール
npm install htmx.org
import 'htmx.org';
unpkgから読み込む
<script src="https://unpkg.com/htmx.org@2.0.8"></script>
正直なところ、CDNから読み込むのが一番手軽でおすすめです。
htmxのサンプルを動かす
説明より先に、実際にhtmxを動かしてみましょう。下のサンプルはボタンを押すとhx-get属性が指定されたURLへリクエストを発行し、レスポンスのHTMLでhx-targetが指す#card要素の中身をhx-swap="innerHTML"で差し替えます。
なお、このLiveCodesサンプルは外部サーバーと通信できないため、実サーバーの代わりにhtmx:beforeRequestイベントをevt.preventDefault()でキャンセルし、あらかじめ<template>に用意しておいたHTMLをhtmx.swap()でその場に注入する「モックサーバー」を仕込んでいます。htmx本体のhx-get・hx-target・hx-swapの挙動そのものは実際のhtmxライブラリが処理しているので、属性の書き方や動きは実サーバーと通信する場合とまったく同じです。
書き方の要点は次のとおりです。
<button hx-get="/mock/user/1" hx-target="#card" hx-swap="innerHTML">
田中さんを表示
</button>
<div id="card">ここにプロフィールが表示されます</div>
// 本来はサーバーがHTMLを返す。ここではhx-getのリクエストを
// htmx:beforeRequestで横取りし、テンプレートのHTMLをhtmx.swap()で注入する
document.body.addEventListener('htmx:beforeRequest', (evt) => {
evt.preventDefault()
const html = document.querySelector('template[data-mock="/mock/user/1"]').innerHTML
htmx.swap(evt.detail.target, html, { swapStyle: 'innerHTML' })
})
実際に動かせるのが下のサンプルです。「田中さんを表示」「佐藤さんを表示」の2つのボタンを押し比べて、#cardの中身がクリックのたびに切り替わる様子を確認してください。
hx-getの値を/mock/user/1から/mock/user/2に書き換えれば表示される人物が変わりますし、hx-swapをouterHTMLに変えれば#card要素ごと置き換わるようになります。htmxの実務では、この/mock/user/1の部分に実際のAPIエンドポイントを指定し、サーバーがHTMLを返すだけでこのサンプルと同じ動きが実現できます。
基本的な使い方
主要な属性
htmxの核となる属性を紹介します。
hx-get / hx-post / hx-put / hx-patch / hx-delete
HTTPリクエストを発行する属性です。
<!-- GETリクエスト -->
<button hx-get="/api/users">ユーザー一覧を取得</button>
<!-- POSTリクエスト -->
<button hx-post="/api/users" hx-vals='{"name": "田中"}'>ユーザーを作成</button>
<!-- DELETEリクエスト -->
<button hx-delete="/api/users/1">ユーザーを削除</button>
hx-target
レスポンスを挿入する場所を指定します。
<button hx-get="/api/data" hx-target="#result">
データを取得
</button>
<div id="result">ここにデータが表示される</div>
CSSセレクタで指定できるので、柔軟性が高い。
hx-swap
レスポンスの挿入方法を指定します。
<!-- 要素の中身を置き換え(デフォルト) -->
<div hx-get="/data" hx-swap="innerHTML">...</div>
<!-- 要素全体を置き換え -->
<div hx-get="/data" hx-swap="outerHTML">...</div>
<!-- 要素の先頭に追加 -->
<div hx-get="/data" hx-swap="beforebegin">...</div>
<!-- 要素の末尾に追加 -->
<div hx-get="/data" hx-swap="beforeend">...</div>
<!-- 要素を削除 -->
<button hx-delete="/item/1" hx-swap="delete">削除</button>
hx-trigger
リクエストのトリガーとなるイベントを指定します。
<!-- クリック時(デフォルト) -->
<button hx-get="/data" hx-trigger="click">クリック</button>
<!-- マウスオーバー時 -->
<div hx-get="/preview" hx-trigger="mouseenter">プレビュー</div>
<!-- 入力後500ms経過時 -->
<input hx-get="/search" hx-trigger="keyup changed delay:500ms" name="q">
<!-- ページ読み込み時 -->
<div hx-get="/initial-data" hx-trigger="load">読み込み中...</div>
<!-- 画面に表示されたとき -->
<div hx-get="/lazy-content" hx-trigger="revealed">遅延読み込み</div>
フォームの送信
<form hx-post="/api/contact" hx-target="#message">
<input type="text" name="name" placeholder="お名前">
<input type="email" name="email" placeholder="メールアドレス">
<textarea name="message" placeholder="メッセージ"></textarea>
<button type="submit">送信</button>
</form>
<div id="message"></div>
バリデーションエラーも、サーバーからHTMLを返せばそのまま表示できます。
インジケーターの表示
<button hx-get="/slow-api" hx-indicator="#spinner">
データを取得
</button>
<span id="spinner" class="htmx-indicator">読み込み中...</span>
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: inline;
}
リクエスト中だけインジケーターを表示できます。これも属性だけで実現できるのが良い。
実践的なユースケース
1. 無限スクロール
<div id="posts">
<!-- 投稿一覧 -->
<div class="post">投稿1</div>
<div class="post">投稿2</div>
<!-- 最後の要素が表示されたら次のページを読み込む -->
<div hx-get="/posts?page=2"
hx-trigger="revealed"
hx-swap="afterend"
hx-indicator="#loading">
</div>
</div>
<div id="loading" class="htmx-indicator">読み込み中...</div>
サーバー側は次の投稿とともに、さらに次を読み込むためのトリガー要素を返せばOK。
2. ライブ検索
<input type="text"
name="q"
hx-get="/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#search-results"
placeholder="検索...">
<div id="search-results"></div>
入力後300ms待ってからリクエストを送信。無駄なリクエストを抑えつつ、リアルタイムに検索結果を表示できます。
3. 削除確認
<button hx-delete="/api/posts/1"
hx-confirm="本当に削除しますか?"
hx-target="closest article"
hx-swap="outerHTML">
削除
</button>
hx-confirmで確認ダイアログを表示。削除後は親の<article>要素ごと消えます。
4. タブ切り替え
<div class="tabs">
<button hx-get="/tab/1" hx-target="#tab-content" class="active">タブ1</button>
<button hx-get="/tab/2" hx-target="#tab-content">タブ2</button>
<button hx-get="/tab/3" hx-target="#tab-content">タブ3</button>
</div>
<div id="tab-content">
<!-- タブの内容 -->
</div>
JavaScriptで状態管理する必要なし。シンプルです。
5. WebSocketでリアルタイム更新
<div hx-ext="ws" ws-connect="/chat">
<div id="messages"></div>
<form ws-send>
<input name="message" placeholder="メッセージを入力">
<button type="submit">送信</button>
</form>
</div>
WebSocket拡張を使えば、チャットのようなリアルタイム機能も実現できます。
6. Server Sent Events(SSE)
<div hx-ext="sse" sse-connect="/events" sse-swap="message">
<!-- サーバーからのイベントで更新される -->
</div>
サーバーからのプッシュ通知も属性一つで対応可能。
ここからは、上のイベント例と同じくブラウザだけで動かせるパターンを、htmxのサンプルとして3つ用意しました。それぞれhx-trigger・hx-swapの使い分けが体験できます(外部サーバーとは通信しないため、htmx:beforeRequestをキャンセルしてHTMLを返すモックを内蔵しています)。
htmxのhx-triggerで入力ごとに検証する
フォーム入力のたびにサーバー検証を行いたい場面で使うのがhx-trigger="keyup changed delay:300ms"です。キー入力から300ms操作が止まったタイミングでリクエストが発行され、レスポンスのHTMLでhx-target先を差し替えます。ライブ検索やリアルタイムバリデーションでよく使われる書き方です。
<input hx-post="/mock/validate"
hx-trigger="keyup changed delay:300ms"
hx-target="#validation-result">
// hx-trigger で入力のたびに(300ms デバウンスして)リクエストが飛ぶ。
// ここではサーバーの代わりに正規表現でHTMLを組み立てて返す
document.body.addEventListener('htmx:beforeRequest', (evt) => {
evt.preventDefault()
const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(evt.detail.elt.value)
htmx.swap(evt.detail.target, valid ? '✓ OK' : '✗ NG', { swapStyle: 'innerHTML' })
})
実際に入力欄を書き換えて試せるのが下のサンプルです。初期値は有効なメールアドレスなので緑のチェックが出ますが、@を消したり空にしたりすると赤いエラー表示に変わります。
delay:300msの数値を0にすると、キーを押すたびに即座に検証が走るようになります(実サーバーだと打鍵ごとにリクエストが飛んで負荷が上がるので、通常はデバウンスを入れておくのがおすすめです)。
htmxのhx-swapで要素を追加・削除する
hx-swapは値によって挿入の仕方が変わります。beforeendは対象要素の末尾に追加、deleteはレスポンスを使わず対象要素そのものをDOMから削除します。1つのタスクリストで、追加と削除の両方のhx-swapパターンを試せます。
<button hx-post="/mock/todo" hx-target="#todo-list" hx-swap="beforeend">追加</button>
<button hx-delete="/mock/todo/1" hx-target="closest li" hx-swap="delete">削除</button>
// hx-swap="beforeend" は末尾へ追加、hx-swap="delete" は要素そのものを削除する
document.body.addEventListener('htmx:beforeRequest', (evt) => {
evt.preventDefault()
const swapStyle = evt.detail.elt.getAttribute('hx-swap')
htmx.swap(evt.detail.target, swapStyle === 'delete' ? '' : '<li>新しいタスク</li>', { swapStyle })
})
下のサンプルで「タスクを追加」を押すとリストの末尾に項目が増え、各項目の「削除」を押すとその項目だけがhx-swap="delete"で消えます。
新しく追加された<li>内の削除ボタンにもhx-deleteとhx-swap="delete"が効いているのがポイントです。動的に挿入したHTMLに対してはhtmx.process()を呼んで初期化しないと、htmxの属性が認識されません。
htmxのhx-triggerでポーリングする(周期更新)
hx-trigger="every 2s"のように書くと、htmxが指定間隔でリクエストを自動的に繰り返します。ダッシュボードの自動更新や通知バッジのポーリングに使われる書き方です。
<div id="clock" hx-get="/mock/time" hx-trigger="every 2s" hx-swap="innerHTML">
読み込み中...
</div>
// hx-trigger="every 2s" で2秒ごとに自動リクエスト。モックが現在時刻を返す
document.body.addEventListener('htmx:beforeRequest', (evt) => {
evt.preventDefault()
htmx.swap(evt.detail.target, new Date().toLocaleTimeString('ja-JP'), { swapStyle: 'innerHTML' })
})
下のサンプルを開くと、#clockの表示が2秒ごとに現在時刻へ自動更新され続けます。何もクリックしなくてもhx-triggerだけで定期更新が起きる点を確認してみてください。
間隔をevery 2sからevery 500msに変えると更新頻度が上がり、hx-triggerの秒数指定だけでポーリング速度をコントロールできることが体感できます。
拡張機能
htmxには便利な拡張機能が用意されています。
json-enc - JSONでリクエストボディを送信
<script src="https://unpkg.com/htmx.org@2.0.8/dist/ext/json-enc.js"></script>
<form hx-post="/api/users" hx-ext="json-enc">
<input name="name" value="田中">
<button>送信</button>
</form>
フォームデータをJSONとして送信できます。
preload - リンクのプリロード
<script src="https://unpkg.com/htmx.org@2.0.8/dist/ext/preload.js"></script>
<a href="/page" hx-get="/page" preload="mouseover">
ホバーでプリロード
</a>
マウスオーバーで先読みしておくことで、体感速度が向上します。
注意点
いいことばかり書いてきましたが、htmxにも向き不向きがあります。
向いているケース
- 既存のサーバーサイドアプリに動的機能を追加したい
- 管理画面やダッシュボードの構築
- コンテンツ主体のWebサイト
- プロトタイプの素早い開発
- JavaScriptを最小限にしたい
向いていないケース
- 複雑な状態管理が必要なSPA
- オフライン対応が必要なアプリ
- 高度なアニメーションやインタラクション
- クライアント側で複雑な計算が必要な場合
個人的には、「8割のWebアプリはhtmxで十分」だと思っています。残りの2割(高度なSPA)にはReactやVueを使えばいい。
まとめ
正直なところ、htmxは「Webの原点回帰」という感じがしますね。
HTMLが本来持つべきだった能力を取り戻す。JavaScriptで複雑なことをやらなくても、シンプルなHTML属性で多くのことが実現できる。
以下の条件に当てはまるなら、試してみる価値は間違いなくあります:
- JavaScriptフレームワークの複雑さに疲れている
- サーバーサイドの言語を活かしたい
- 学習コストを抑えてモダンなUIを作りたい
- 既存のアプリに動的機能を追加したい
- シンプルなものが好き
46,700スター以上という数字が、その実用性を証明しています。
まずは既存のサイトに<script>タグを一行追加して、ボタン一つから試してみてください。その手軽さに驚くはずです。
公式サイト: https://htmx.org GitHub: https://github.com/bigskysoftware/htmx ドキュメント: https://htmx.org/docs