2026/04/03
htmx とは
HTML 属性を追加するだけで、通常 JavaScript で書く必要がある Ajax、部分更新などのインタラクティブな動作を実現できる JavaScript ライブラリ
JavaScript をほとんど書かずに動的な UI を実現できる
<!-- これだけで Ajax でデータを取得して div を更新できる -->
<button hx-get="/api/users" hx-target="#result">
ユーザーを読み込む
</button>
<div id="result"></div>
従来との違い
htmx は MPA のシンプルさを保ちながら、SPA のような動的な UI を実現する、中間の第三の選択肢的存在
【MPA】
ページ遷移のたびにサーバーから HTML 全体を取得して全画面再描画する
→ 画面がちらつく、遷移のたびに待ち時間が発生する
→ 動的な部分更新には JavaScript を自前で書く必要がある
【MPA + htmx】
ページ全体ではなく「変化した部分の HTML」だけをサーバーから受け取る
→ ちらつかない、JavaScript をほとんど書かない
→ サーバーは HTML を返す(JSON ではなく)
→ ルーティング・状態管理をサーバーサイドで完結させられる
比較(MPA, MPA+htmx, SPA)
| MPA | MPA + htmx | SPA(React・Vue) | |
|---|---|---|---|
| ページ遷移 | 全画面再描画 | 部分的に HTML を差し替え | JavaScript で DOM を書き換え |
| サーバーが返すもの | HTML | HTML(部分) | JSON |
| JavaScript の量 | 少ない | 少ない | 多い |
| ビルドステップ | 不要 | 不要 | 必要(webpack・Vite 等) |
| SEO 対応 | ◎ | ◎ | △(SSR が必要) |
| 初回ロード | 速い | 速い | 遅め |
| 複雑な UI | △ | ○ | ◎ |
基本構文・使い方
セットアップ
<!-- CDN から読み込む -->
<script src="https://unpkg.com/htmx.org@2.0.0"></script>
<!-- または npm でインストール -->
npm install htmx.org
主要な属性
下記一例
| 属性 | 概要 |
|---|---|
hx-get | GET リクエストを送信する |
hx-post | POST リクエストを送信する |
hx-put | PUT リクエストを送信する |
hx-delete | DELETE リクエストを送信する |
hx-target | レスポンスを挿入する対象要素 |
hx-swap | 挿入方法(innerHTML / outerHTML等) |
基本パターン
<!-- ① ボタンクリックで部分更新 -->
<button hx-get="/users/list"
hx-target="#user-list"
hx-swap="innerHTML">
ユーザー一覧を読み込む
</button>
<div id="user-list"><!-- ここに結果が挿入される --></div>
<!-- ② フォーム送信 -->
<form hx-post="/users"
hx-target="#result"
hx-swap="outerHTML">
<input type="text" name="name" placeholder="名前">
<button type="submit">登録</button>
</form>
<div id="result"></div>
<!-- ③ 削除ボタン -->
<tr id="user-1">
<td>Taro</td>
<td>
<button hx-delete="/users/1"
hx-target="#user-1"
hx-swap="outerHTML"
hx-confirm="本当に削除しますか?">
削除
</button>
</td>
</tr>
レスポンス
サーバーは完全な HTML ではなく、部分的な HTML(フラグメント) を返す
# Rails
class UsersController < ApplicationController
def index
@users = User.all
respond_to do |format|
format.html # 通常のページ表示(index.html.erb)
format.any { render partial: 'users/list', locals: { users: @users } }
# htmx からのリクエストには部分 HTML を返す
end
end
def create
@user = User.new(user_params)
if @user.save
# 新しく追加されたユーザーの行だけを返す
render partial: 'users/user_row', locals: { user: @user }
else
# エラーフォームを返す(HTTP ステータス 422)
render partial: 'users/form', locals: { user: @user },
status: :unprocessable_entity
end
end
def destroy
User.find(params[:id]).destroy
# 削除後は空レスポンス(hx-swap="outerHTML" で行が消える)
head :ok
end
end
<%# app/views/users/_user_row.html.erb(部分テンプレート) %>
<tr id="user-<%= user.id %>">
<td><%= user.name %></td>
<td><%= user.email %></td>
<td>
<button hx-delete="/users/<%= user.id %>"
hx-target="#user-<%= user.id %>"
hx-swap="outerHTML"
hx-confirm="削除しますか?">
削除
</button>
</td>
</tr>
リクエストヘッダー
自動的に以下ヘッダーをリクエストに付与する
これにより、サーバー側で「htmx からのリクエストか通常のリクエストか」を判別できる
# htmx リクエストの判別
request.headers['HX-Request'] # => "true"(htmx からのリクエスト)
request.headers['HX-Target'] # => "user-list"(ターゲット要素の ID)
request.headers['HX-Trigger'] # => "search-input"(トリガー要素の ID)
request.headers['HX-Current-URL'] # => 現在のページの URL
request.headers['HX-Trigger-Name'] # => トリガー要素の name 属性
# Rails
def htmx_request?
request.headers['HX-Request'] == 'true'
end
def index
@users = User.search(params[:q])
if htmx_request?
render partial: 'users/list' # 部分 HTML
else
render :index # フルページ
end
end
ユースケース
向いている場面
- 複雑な UI より「動く」ことが重要
- フロントエンド専任がいないチームで開発する
- 開発速度重視
- 既存の MPA(Rails・Laravel)に動的な要素を追加したい
- フルリライトなしで部分的にインタラクティブにできる
向いていない場面
- 複雑なクライアント状態管理が必要なアプリ(ショッピングカート・複雑なフォームなど)
- 大規模・複雑な UI コンポーネントが必要なアプリ
- リアルタイム性が高いアプリ(チャット・ゲームなど)
- WebSocket ベースのリアルタイム通信が主役の場合
- モバイルアプリと API を共用したい場合
参考
- htmx:https://htmx.org
- htmx doc:https://htmx.org/docs/
- htmx reference:https://htmx.org/reference/