htmx 入門:ゼロから始める次世代フロントエンド開発

フロントエンド開発の世界は、ここ数年で驚くほど複雑になりました。React、Vue.js、Angular といった強力なフレームワークが主流となる一方で、多くの開発者が「学習コストの高さ」や「環境構築の複雑さ」に悩んでいるのではないでしょうか。
「簡単な機能を追加したいだけなのに、なぜこんなに複雑な設定が必要なの?」 「新しいフレームワークを覚える時間がない...」 「もっとシンプルに開発できる方法はないの?」
そんな悩みを抱える方に朗報です。htmx という革新的なライブラリが、フロントエンド開発の常識を変えようとしています。わずか 14KB の軽量ライブラリでありながら、HTML 属性だけでリッチなインタラクションを実現できる次世代のアプローチです。
今回は、htmx を完全にゼロから学び、実際に動作するアプリケーションを構築するまでの道のりを、初心者の方にもわかりやすくご紹介します。
フロントエンド開発の現在地
JavaScript フレームワークの複雑化
現代の Web 開発では、React、Vue.js、Angular といったフレームワークが圧倒的なシェアを占めています。これらのフレームワークは確かに強力ですが、同時に多くの課題も抱えています。
React プロジェクトの典型的な構成を見てみましょう:
bash# React プロジェクトの作成
yarn create react-app my-app
cd my-app
# よく必要になる追加パッケージ
yarn add react-router-dom
yarn add @reduxjs/toolkit react-redux
yarn add axios
yarn add @mui/material @emotion/react @emotion/styled
yarn add @testing-library/react @testing-library/jest-dom
この時点で、プロジェクトのサイズは既に数百 MB に達し、node_modules
フォルダーには数千のパッケージが含まれています。
学習コストの高騰
現代のフロントエンド開発者が習得すべき技術は膨大です:
# | 技術領域 | 必要な知識 |
---|---|---|
1 | 言語基礎 | JavaScript ES6+、TypeScript |
2 | フレームワーク | React Hooks、Vue Composition API |
3 | 状態管理 | Redux、Zustand、Pinia |
4 | ルーティング | React Router、Vue Router |
5 | ビルドツール | Webpack、Vite、Rollup |
6 | テスト | Jest、Testing Library、Cypress |
7 | スタイリング | CSS-in-JS、Tailwind CSS、Sass |
新人エンジニアがこれらを習得するには、12〜18 ヶ月もの期間が必要とされています。
開発効率の課題
実際の開発現場では、以下のような問題が頻発しています:
環境構築で発生する典型的なエラー:
bash# Node.js バージョン不整合エラー
error @typescript-eslint/eslint-plugin@5.59.0:
The engine "node" is incompatible with this module.
Expected version "^12.22.0 || ^14.17.0 || >=16.0.0".
# 依存関係の競合エラー
npm ERR! peer dep missing: react@">=16.8.0", required by @testing-library/react-hooks@8.0.1
# ビルドエラー
Module build failed (from ./node_modules/babel-loader/lib/index.js):
SyntaxError: Support for the experimental syntax 'jsx' isn't currently enabled
これらのエラーの解決に、本来の開発時間の 30〜40% を費やしてしまうケースも珍しくありません。
メンテナンスの負担も深刻です。依存関係の更新だけで以下のような作業が必要になります:
bash# セキュリティ脆弱性の警告
$ yarn audit
Found 47 vulnerabilities (12 moderate, 35 high)
# パッケージの更新作業
$ yarn upgrade-interactive --latest
# 数十のパッケージの更新判断が必要...
htmx という新しい選択肢
htmx とは何か
htmx(HyperText Markup eXtensions)は、2020 年に登場した革新的な JavaScript ライブラリです。**「HTML を中心とした Web 開発」**という、従来のフレームワークとは正反対のアプローチを提案しています。
htmx の基本思想は非常にシンプルです:
HTML は本来、ハイパーメディアとして強力な能力を持っている。 その能力を現代的に拡張すれば、複雑な JavaScript は不要になる。
従来の開発 vs htmx 開発の比較:
javascript// 従来のJavaScript(Ajax通信)
fetch('/api/users')
.then((response) => response.json())
.then((data) => {
const userList = document.getElementById('users');
userList.innerHTML = data
.map((user) => `<li>${user.name}</li>`)
.join('');
})
.catch((error) => {
console.error('Error:', error);
});
html<!-- htmx(HTML属性のみ) -->
<button hx-get="/api/users" hx-target="#users">
ユーザー一覧を取得
</button>
<ul id="users"></ul>
この違いは一目瞭然です。htmx では HTML 属性だけで Ajax 通信が実現できています。
従来のアプローチとの違い
htmx と従来のフレームワークの根本的な違いを表で整理してみましょう:
# | 項目 | 従来のフレームワーク | htmx |
---|---|---|---|
1 | 中心となる技術 | JavaScript | HTML |
2 | データのやり取り | JSON | HTML フラグメント |
3 | 状態管理 | クライアントサイド | サーバーサイド |
4 | 学習コスト | 高い(6〜12 ヶ月) | 低い(1〜2 週間) |
5 | バンドルサイズ | 数百 KB〜数 MB | 14KB |
6 | 既存プロジェクトへの導入 | 困難(リライトが必要) | 容易(段階的導入可能) |
特に注目すべきはサーバーサイドでの状態管理です。従来の SPA では、クライアント側で複雑な状態管理を行う必要がありましたが、htmx ではサーバーが HTML を返すだけのシンプルな構造になります。
なぜ「次世代」なのか
htmx が「次世代」と呼ばれる理由は、単に新しいからではありません。Web 開発の本質に立ち返り、複雑さを取り除いたからです。
1. 学習曲線の劇的な改善
従来のフレームワークの学習曲線:
markdown複雑さ
^
| ●●●●●●●● (React習得)
| ●●
| ●●
●●________________________> 時間
0 6ヶ月 12ヶ月
htmx の学習曲線:
markdown複雑さ
^
|●●●●●●●●●●●●●●●●●●● (すぐに使える)
|
|
●●________________________> 時間
0 1週間 2週間
2. 開発生産性の向上
実際のプロジェクトでの開発時間比較:
# | 機能 | React 開発時間 | htmx 開発時間 |
---|---|---|---|
1 | 環境構築 | 2〜4 時間 | 5 分 |
2 | シンプルなフォーム | 1〜2 時間 | 15 分 |
3 | Ajax 通信 | 30 分〜1 時間 | 5 分 |
4 | 動的リスト更新 | 1〜2 時間 | 20 分 |
3. メンテナンス性の向上
htmx プロジェクトの package.json
:
json{
"dependencies": {
"express": "^4.18.2",
"htmx.org": "^1.9.10"
}
}
たった 2 つの依存関係です。セキュリティ脆弱性の心配も、複雑な更新作業も大幅に軽減されます。
4. パフォーマンスの優位性
- 初期読み込み:14KB vs 数百 KB〜数 MB
- ランタイム:DOM 操作のみ vs 仮想 DOM + 状態管理
- メモリ使用量:最小限 vs フレームワーク + ライブラリ群
これらの特徴により、htmx は Web 開発を「原点回帰」させながら、同時に「次世代」の開発体験を提供しているのです。
はじめての htmx:基本の「き」
環境構築(最小構成)
htmx の最大の魅力は、環境構築の簡単さです。従来のフレームワークのような複雑な設定は一切必要ありません。
方法 1:CDN を使用した最速セットアップ
まずは、最も簡単な方法から始めましょう。HTML ファイル一つあれば十分です。
index.html:
html<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>htmx 入門</title>
<!-- htmx をCDNから読み込む(これだけ!) -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
</head>
<body>
<h1>htmx 入門</h1>
<p>準備完了です!</p>
</body>
</html>
これだけで htmx を使う準備が整いました。所要時間:1 分
方法 2:Yarn を使用したプロジェクト構成
実際の開発では、サーバーサイドと組み合わせることが多いので、完全なプロジェクト構成も見てみましょう。
bash# プロジェクトの作成
mkdir htmx-tutorial
cd htmx-tutorial
yarn init -y
# 必要な依存関係をインストール
yarn add htmx.org express
yarn add -D nodemon
package.json の設定:
json{
"name": "htmx-tutorial",
"version": "1.0.0",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"htmx.org": "^1.9.10"
},
"devDependencies": {
"nodemon": "^3.0.2"
}
}
server.js:
javascriptconst express = require('express');
const path = require('path');
const app = express();
const PORT = 3000;
// 静的ファイルの配信
app.use(express.static('public'));
// メインページ
app.get('/', (req, res) => {
res.sendFile(
path.join(__dirname, 'public', 'index.html')
);
});
app.listen(PORT, () => {
console.log(
`サーバーが http://localhost:${PORT} で起動しました`
);
});
public/index.html:
html<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>htmx Tutorial</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
</head>
<body>
<h1>htmx チュートリアル</h1>
<p>サーバーが正常に動作しています!</p>
</body>
</html>
サーバーを起動:
bashyarn dev
所要時間:5 分
最初の一歩:ボタンクリックで動的更新
環境が整ったので、早速 htmx の基本機能を体験してみましょう。
シンプルな時刻表示機能
server.js に API エンドポイントを追加:
javascript// ... existing code ...
// 現在時刻を返す API
app.get('/api/time', (req, res) => {
const now = new Date().toLocaleString('ja-JP');
res.send(`<p>現在時刻: <strong>${now}</strong></p>`);
});
// ... existing code ...
public/index.html を更新:
html<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>htmx Tutorial</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
}
button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
#time-display {
margin-top: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
}
</style>
</head>
<body>
<h1>htmx チュートリアル</h1>
<!-- これがhtmxの魔法! -->
<button hx-get="/api/time" hx-target="#time-display">
現在時刻を取得
</button>
<div id="time-display">ここに時刻が表示されます</div>
</body>
</html>
ボタンをクリックしてみてください。JavaScript を一行も書かずに Ajax 通信が実行され、現在時刻が動的に表示されます!
HTML 属性の基本概念
先ほどの例で使用した hx-get
と hx-target
について詳しく見てみましょう。
hx-get 属性
hx-get
は HTTP GET リクエストを送信する属性です:
html<!-- 基本的な使用法 -->
<button hx-get="/api/data">データを取得</button>
<!-- 任意の要素で使用可能 -->
<div hx-get="/api/content">この div をクリックでも動作</div>
<span hx-get="/api/info">この span でも動作</span>
hx-target 属性
hx-target
はレスポンスを挿入する場所を指定します:
html<!-- ID で指定 -->
<button hx-get="/api/data" hx-target="#result">取得</button>
<div id="result"></div>
<!-- CSS セレクターで指定 -->
<button hx-get="/api/data" hx-target=".content-area">
取得
</button>
<div class="content-area"></div>
<!-- 相対的な指定 -->
<button hx-get="/api/data" hx-target="next div">
取得
</button>
<div>ここに挿入される</div>
よくあるエラーとその解決法
初学者がよく遭遇するエラーをご紹介します:
エラー 1:htmx が読み込まれていない
csharpUncaught ReferenceError: htmx is not defined
解決法:
html<!-- script タグが正しく配置されているか確認 -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
エラー 2:サーバーが起動していない
bashGET http://localhost:3000/api/time net::ERR_CONNECTION_REFUSED
解決法:
bash# サーバーが起動しているか確認
yarn dev
エラー 3:API エンドポイントが存在しない
bashGET http://localhost:3000/api/time 404 (Not Found)
解決法:server.js でエンドポイントが正しく定義されているか確認してください。
ステップアップ:基本機能を理解する
hx-get, hx-post の使い分け
htmx では、様々な HTTP メソッドを HTML 属性として使用できます。
hx-get:データの取得
hx-get
はデータを取得する際に使用します:
html<!-- ユーザー情報の取得 -->
<button hx-get="/api/user/123" hx-target="#user-info">
ユーザー情報を表示
</button>
<!-- 検索結果の取得 -->
<input
type="text"
hx-get="/api/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#search-results"
placeholder="検索キーワード"
/>
hx-post:データの送信
hx-post
はデータを送信する際に使用します:
html<!-- フォームの送信 -->
<form hx-post="/api/contact" hx-target="#result">
<input name="name" placeholder="お名前" required />
<input
name="email"
type="email"
placeholder="メールアドレス"
required
/>
<textarea
name="message"
placeholder="メッセージ"
></textarea>
<button type="submit">送信</button>
</form>
<div id="result"></div>
server.js でのフォーム処理:
javascript// フォームデータの解析を有効化
app.use(express.urlencoded({ extended: true }));
// フォーム送信の処理
app.post('/api/contact', (req, res) => {
const { name, email, message } = req.body;
// 簡単なバリデーション
if (!name || !email || !message) {
res.status(400).send(`
<div style="color: red; padding: 10px; border: 1px solid red;">
エラー: すべての項目を入力してください
</div>
`);
return;
}
// 成功レスポンス
res.send(`
<div style="color: green; padding: 10px; border: 1px solid green;">
${name}さん、お問い合わせありがとうございます!<br>
${email} に確認メールを送信しました。
</div>
`);
});
hx-target でのターゲット指定
hx-target
は非常に柔軟なターゲット指定が可能です。
基本的な指定方法
html<!-- ID での指定 -->
<button hx-get="/api/data" hx-target="#result">取得</button>
<!-- クラスでの指定 -->
<button hx-get="/api/data" hx-target=".result-area">
取得
</button>
<!-- 要素タイプでの指定 -->
<button hx-get="/api/data" hx-target="p">取得</button>
相対的な指定
html<!-- 最も近い親要素 -->
<div>
<button hx-get="/api/data" hx-target="closest div">
更新
</button>
<p>この div が更新されます</p>
</div>
<!-- 次の兄弟要素 -->
<button hx-get="/api/data" hx-target="next .result">
取得
</button>
<div class="result">ここが更新されます</div>
<!-- 前の兄弟要素 -->
<div class="result">ここが更新されます</div>
<button hx-get="/api/data" hx-target="previous .result">
取得
</button>
hx-trigger でのイベント制御
hx-trigger
を使うことで、リクエストを発火するタイミングを細かく制御できます。
基本的なイベント
html<!-- クリック時(デフォルト) -->
<button hx-get="/api/data">クリック</button>
<!-- マウスオーバー時 -->
<div hx-get="/api/preview" hx-trigger="mouseenter">
ホバーして詳細を表示
</div>
<!-- フォーカス時 -->
<input hx-get="/api/suggestions" hx-trigger="focus" />
<!-- 値変更時 -->
<select hx-get="/api/filter" hx-trigger="change">
<option value="all">すべて</option>
<option value="active">アクティブ</option>
</select>
高度なトリガー設定
html<!-- 遅延実行 -->
<input
hx-get="/api/search"
hx-trigger="keyup changed delay:500ms"
placeholder="入力後0.5秒で検索"
/>
<!-- 複数イベント -->
<div hx-get="/api/data" hx-trigger="click, keyup from:body">
クリックまたはキー入力で実行
</div>
<!-- 条件付き実行 -->
<button
hx-get="/api/premium-feature"
hx-trigger="click[this.classList.contains('premium')]"
>
プレミアム機能
</button>
<!-- 定期実行 -->
<div
hx-get="/api/status"
hx-trigger="every 30s"
hx-target="this"
>
30秒ごとに更新されるステータス
</div>
実践的な検索機能の実装
これまでの知識を組み合わせて、リアルタイム検索機能を作ってみましょう:
server.js に検索 API を追加:
javascript// サンプルデータ
const users = [
{
id: 1,
name: '田中太郎',
email: 'tanaka@example.com',
role: 'エンジニア',
},
{
id: 2,
name: '佐藤花子',
email: 'sato@example.com',
role: 'デザイナー',
},
{
id: 3,
name: '鈴木一郎',
email: 'suzuki@example.com',
role: 'マネージャー',
},
{
id: 4,
name: '高橋美咲',
email: 'takahashi@example.com',
role: 'エンジニア',
},
{
id: 5,
name: '山田健太',
email: 'yamada@example.com',
role: 'セールス',
},
];
// 検索 API
app.get('/api/search', (req, res) => {
const query = req.query.q || '';
if (query.trim() === '') {
res.send('<p>検索キーワードを入力してください</p>');
return;
}
const results = users.filter(
(user) =>
user.name.includes(query) ||
user.email.includes(query) ||
user.role.includes(query)
);
if (results.length === 0) {
res.send(
`<p>「${query}」に一致するユーザーが見つかりませんでした</p>`
);
return;
}
const html = results
.map(
(user) => `
<div style="border: 1px solid #ddd; padding: 10px; margin: 5px 0;">
<strong>${user.name}</strong> (${user.role})<br>
<small>${user.email}</small>
</div>
`
)
.join('');
res.send(html);
});
HTML 部分:
html<div>
<h2>ユーザー検索</h2>
<input
type="text"
name="q"
hx-get="/api/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#search-results"
hx-include="this"
placeholder="名前、メール、役職で検索..."
style="width: 100%; padding: 10px; font-size: 16px;"
/>
<div id="search-results" style="margin-top: 20px;">
検索キーワードを入力してください
</div>
</div>
この実装により、300ms の遅延付きリアルタイム検索が実現できます。ユーザーが入力を止めてから 300ms 後に検索が実行されるため、無駄なリクエストを避けることができます。
実践編:小さなアプリを作ってみる
カウンターアプリの作成
htmx の基本を理解したところで、実際にインタラクティブなアプリケーションを作ってみましょう。まずはシンプルなカウンターアプリから始めます。
サーバーサイドの実装
server.js にカウンター機能を追加:
javascript// カウンターの状態(実際の開発ではデータベースを使用)
let counter = 0;
// カウンター表示
app.get('/api/counter', (req, res) => {
res.send(`
<div style="text-align: center; font-size: 24px; padding: 20px;">
現在のカウント: <strong>${counter}</strong>
</div>
`);
});
// カウンターを増加
app.post('/api/counter/increment', (req, res) => {
counter++;
res.send(`
<div style="text-align: center; font-size: 24px; padding: 20px;">
現在のカウント: <strong>${counter}</strong>
</div>
`);
});
// カウンターを減少
app.post('/api/counter/decrement', (req, res) => {
counter--;
res.send(`
<div style="text-align: center; font-size: 24px; padding: 20px;">
現在のカウント: <strong>${counter}</strong>
</div>
`);
});
// カウンターをリセット
app.post('/api/counter/reset', (req, res) => {
counter = 0;
res.send(`
<div style="text-align: center; font-size: 24px; padding: 20px;">
現在のカウント: <strong>${counter}</strong>
</div>
`);
});
フロントエンドの実装
public/counter.html を作成:
html<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>htmx カウンターアプリ</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 40px;
text-align: center;
}
.counter-display {
background: #f8f9fa;
border: 2px solid #dee2e6;
border-radius: 8px;
margin: 20px 0;
}
.button-group {
display: flex;
justify-content: center;
gap: 10px;
margin: 20px 0;
}
button {
padding: 12px 24px;
font-size: 16px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-primary:hover {
background: #0056b3;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #545b62;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
</style>
</head>
<body>
<h1>htmx カウンターアプリ</h1>
<!-- カウンター表示エリア -->
<div
id="counter-display"
class="counter-display"
hx-get="/api/counter"
hx-trigger="load"
>
読み込み中...
</div>
<!-- ボタングループ -->
<div class="button-group">
<button
class="btn-primary"
hx-post="/api/counter/increment"
hx-target="#counter-display"
>
➕ 増加
</button>
<button
class="btn-secondary"
hx-post="/api/counter/decrement"
hx-target="#counter-display"
>
➖ 減少
</button>
<button
class="btn-danger"
hx-post="/api/counter/reset"
hx-target="#counter-display"
>
🔄 リセット
</button>
</div>
</body>
</html>
この実装のポイント:
- hx-trigger="load":ページ読み込み時に初期値を取得
- 状態管理:サーバーサイドで状態を管理(シンプル!)
- 再利用可能:各ボタンが同じターゲットを更新
フォーム送信とバリデーション
次に、より実用的なフォーム処理を実装してみましょう。ユーザー登録フォームを作成します。
バリデーション付きフォーム処理
server.js にユーザー登録機能を追加:
javascript// ユーザーデータの保存(実際の開発ではデータベースを使用)
const users = [];
// ユーザー登録処理
app.post('/api/users', (req, res) => {
const { name, email, age } = req.body;
// バリデーションエラーのチェック
const errors = [];
if (!name || name.trim().length < 2) {
errors.push('名前は2文字以上で入力してください');
}
if (!email || !email.includes('@')) {
errors.push('有効なメールアドレスを入力してください');
}
if (!age || isNaN(age) || age < 0 || age > 150) {
errors.push('年齢は0〜150の数値で入力してください');
}
// 重複チェック
if (users.find((user) => user.email === email)) {
errors.push('このメールアドレスは既に登録されています');
}
// エラーがある場合
if (errors.length > 0) {
const errorHtml = errors
.map(
(error) => `<li style="color: red;">${error}</li>`
)
.join('');
res.status(400).send(`
<div style="border: 1px solid red; background: #ffe6e6; padding: 15px; border-radius: 4px;">
<h4 style="color: red; margin: 0 0 10px 0;">入力エラー</h4>
<ul style="margin: 0; padding-left: 20px;">
${errorHtml}
</ul>
</div>
`);
return;
}
// ユーザーを追加
const newUser = {
id: users.length + 1,
name: name.trim(),
email: email.trim(),
age: parseInt(age),
createdAt: new Date().toLocaleString('ja-JP'),
};
users.push(newUser);
// 成功レスポンス
res.send(`
<div style="border: 1px solid green; background: #e6ffe6; padding: 15px; border-radius: 4px;">
<h4 style="color: green; margin: 0 0 10px 0;">登録完了</h4>
<p><strong>${newUser.name}</strong>さんの登録が完了しました!</p>
<p>登録日時: ${newUser.createdAt}</p>
<button onclick="location.reload()" style="margin-top: 10px; padding: 8px 16px;">
新規登録を続ける
</button>
</div>
`);
});
// ユーザー一覧取得
app.get('/api/users', (req, res) => {
if (users.length === 0) {
res.send('<p>まだユーザーが登録されていません</p>');
return;
}
const userHtml = users
.map(
(user) => `
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.email}</td>
<td>${user.age}</td>
<td>${user.createdAt}</td>
</tr>
`
)
.join('');
res.send(`
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background: #f8f9fa;">
<th style="border: 1px solid #ddd; padding: 8px;">ID</th>
<th style="border: 1px solid #ddd; padding: 8px;">名前</th>
<th style="border: 1px solid #ddd; padding: 8px;">メール</th>
<th style="border: 1px solid #ddd; padding: 8px;">年齢</th>
<th style="border: 1px solid #ddd; padding: 8px;">登録日時</th>
</tr>
</thead>
<tbody>
${userHtml}
</tbody>
</table>
`);
});
フォームの HTML
public/form.html を作成:
html<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>htmx フォームデモ</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input,
textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
button {
background: #28a745;
color: white;
padding: 12px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background: #218838;
}
button:disabled {
background: #6c757d;
cursor: not-allowed;
}
</style>
</head>
<body>
<h1>ユーザー登録フォーム</h1>
<!-- 登録フォーム -->
<form hx-post="/api/users" hx-target="#form-result">
<div class="form-group">
<label for="name">名前 *</label>
<input
type="text"
id="name"
name="name"
placeholder="山田太郎"
required
/>
</div>
<div class="form-group">
<label for="email">メールアドレス *</label>
<input
type="email"
id="email"
name="email"
placeholder="yamada@example.com"
required
/>
</div>
<div class="form-group">
<label for="age">年齢 *</label>
<input
type="number"
id="age"
name="age"
min="0"
max="150"
placeholder="25"
required
/>
</div>
<button type="submit">登録</button>
</form>
<!-- 結果表示エリア -->
<div id="form-result" style="margin-top: 20px;"></div>
<hr style="margin: 40px 0;" />
<!-- ユーザー一覧 -->
<h2>登録済みユーザー</h2>
<button
hx-get="/api/users"
hx-target="#user-list"
style="background: #007bff;"
>
一覧を表示
</button>
<div id="user-list" style="margin-top: 20px;"></div>
</body>
</html>
動的リスト操作
最後に、リアルタイムでアイテムの追加・削除・編集ができる TODO アプリを作成しましょう。
TODO アプリのサーバーサイド
server.js に TODO 機能を追加:
javascript// TODO データ
let todos = [
{ id: 1, text: 'htmx を学ぶ', completed: false },
{ id: 2, text: 'デモアプリを作る', completed: true },
];
let nextTodoId = 3;
// TODO 一覧取得
app.get('/api/todos', (req, res) => {
const todoHtml = todos
.map(
(todo) => `
<li id="todo-${todo.id}" style="
display: flex;
align-items: center;
padding: 10px;
border: 1px solid #ddd;
margin: 5px 0;
border-radius: 4px;
${
todo.completed
? 'background: #f8f9fa; text-decoration: line-through;'
: ''
}
">
<span style="flex: 1;">${todo.text}</span>
<button
hx-patch="/api/todos/${todo.id}/toggle"
hx-target="#todo-${todo.id}"
style="margin: 0 5px; padding: 5px 10px; border: none; border-radius: 3px; cursor: pointer; ${
todo.completed
? 'background: #ffc107; color: black;'
: 'background: #28a745; color: white;'
}">
${todo.completed ? '戻す' : '完了'}
</button>
<button
hx-delete="/api/todos/${todo.id}"
hx-target="#todo-${todo.id}"
hx-swap="outerHTML"
hx-confirm="本当に削除しますか?"
style="margin: 0 5px; padding: 5px 10px; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
削除
</button>
</li>
`
)
.join('');
res.send(
todos.length > 0 ? todoHtml : '<p>TODO がありません</p>'
);
});
// TODO 追加
app.post('/api/todos', (req, res) => {
const { text } = req.body;
if (!text || text.trim() === '') {
res.status(400).send(`
<div style="color: red; padding: 10px; border: 1px solid red; border-radius: 4px;">
TODO の内容を入力してください
</div>
`);
return;
}
const newTodo = {
id: nextTodoId++,
text: text.trim(),
completed: false,
};
todos.push(newTodo);
// 新しい TODO アイテムを返す
res.send(`
<li id="todo-${newTodo.id}" style="
display: flex;
align-items: center;
padding: 10px;
border: 1px solid #ddd;
margin: 5px 0;
border-radius: 4px;
">
<span style="flex: 1;">${newTodo.text}</span>
<button
hx-patch="/api/todos/${newTodo.id}/toggle"
hx-target="#todo-${newTodo.id}"
style="margin: 0 5px; padding: 5px 10px; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">
完了
</button>
<button
hx-delete="/api/todos/${newTodo.id}"
hx-target="#todo-${newTodo.id}"
hx-swap="outerHTML"
hx-confirm="本当に削除しますか?"
style="margin: 0 5px; padding: 5px 10px; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
削除
</button>
</li>
`);
});
// TODO 完了状態切り替え
app.patch('/api/todos/:id/toggle', (req, res) => {
const id = parseInt(req.params.id);
const todo = todos.find((t) => t.id === id);
if (!todo) {
res
.status(404)
.send(
'<p style="color: red;">TODO が見つかりません</p>'
);
return;
}
todo.completed = !todo.completed;
// 更新された TODO アイテムを返す
res.send(`
<li id="todo-${todo.id}" style="
display: flex;
align-items: center;
padding: 10px;
border: 1px solid #ddd;
margin: 5px 0;
border-radius: 4px;
${
todo.completed
? 'background: #f8f9fa; text-decoration: line-through;'
: ''
}
">
<span style="flex: 1;">${todo.text}</span>
<button
hx-patch="/api/todos/${todo.id}/toggle"
hx-target="#todo-${todo.id}"
style="margin: 0 5px; padding: 5px 10px; border: none; border-radius: 3px; cursor: pointer; ${
todo.completed
? 'background: #ffc107; color: black;'
: 'background: #28a745; color: white;'
}">
${todo.completed ? '戻す' : '完了'}
</button>
<button
hx-delete="/api/todos/${todo.id}"
hx-target="#todo-${todo.id}"
hx-swap="outerHTML"
hx-confirm="本当に削除しますか?"
style="margin: 0 5px; padding: 5px 10px; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
削除
</button>
</li>
`);
});
// TODO 削除
app.delete('/api/todos/:id', (req, res) => {
const id = parseInt(req.params.id);
todos = todos.filter((t) => t.id !== id);
res.send(''); // 空のレスポンスで要素を削除
});
TODO アプリの HTML
public/todo.html を作成:
html<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>htmx TODO アプリ</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.add-form {
display: flex;
margin-bottom: 20px;
}
.add-form input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px 0 0 4px;
font-size: 16px;
}
.add-form button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
}
#todo-list {
list-style: none;
padding: 0;
}
</style>
</head>
<body>
<h1>htmx TODO アプリ</h1>
<!-- TODO 追加フォーム -->
<form
hx-post="/api/todos"
hx-target="#todo-list"
hx-swap="beforeend"
hx-on::after-request="this.reset()"
class="add-form"
>
<input
type="text"
name="text"
placeholder="新しい TODO を入力..."
required
/>
<button type="submit">追加</button>
</form>
<!-- TODO 一覧 -->
<ul
id="todo-list"
hx-get="/api/todos"
hx-trigger="load"
>
読み込み中...
</ul>
</body>
</html>
より実用的な機能を学ぶ
エラーハンドリング
実際のアプリケーションでは、適切なエラーハンドリングが重要です。htmx でのエラー処理方法を学びましょう。
JavaScript によるエラーハンドリング
html<script>
// レスポンスエラーの処理
document.addEventListener(
'htmx:responseError',
function (event) {
const statusCode = event.detail.xhr.status;
const errorMessage = event.detail.xhr.responseText;
let alertMessage = 'エラーが発生しました';
switch (statusCode) {
case 400:
alertMessage = `入力エラー: ${errorMessage}`;
break;
case 401:
alertMessage =
'認証が必要です。ログインしてください。';
break;
case 403:
alertMessage = 'アクセス権限がありません。';
break;
case 404:
alertMessage =
'要求されたリソースが見つかりません。';
break;
case 500:
alertMessage =
'サーバー内部エラーが発生しました。管理者に連絡してください。';
break;
default:
alertMessage = `HTTP ${statusCode}: ${errorMessage}`;
}
// エラーメッセージを表示
const errorDiv = document.createElement('div');
errorDiv.innerHTML = `
<div style="
position: fixed;
top: 20px;
right: 20px;
background: #f8d7da;
color: #721c24;
padding: 15px;
border: 1px solid #f5c6cb;
border-radius: 4px;
max-width: 300px;
z-index: 1000;
">
<strong>エラー</strong><br>
${alertMessage}
<button onclick="this.parentElement.parentElement.remove()"
style="float: right; background: none; border: none; font-size: 18px;">
×
</button>
</div>
`;
document.body.appendChild(errorDiv);
// 5秒後に自動で削除
setTimeout(() => {
if (errorDiv.parentElement) {
errorDiv.remove();
}
}, 5000);
}
);
// ネットワークエラーの処理
document.addEventListener(
'htmx:sendError',
function (event) {
alert(
'ネットワークエラー: サーバーに接続できません。インターネット接続を確認してください。'
);
}
);
</script>
ローディング状態の表示
ユーザビリティ向上のため、リクエスト中のローディング表示を実装しましょう。
CSS でのローディング表示
css/* ローディング時のスタイル */
.htmx-request {
opacity: 0.6;
pointer-events: none;
}
.htmx-request::after {
content: ' 読み込み中...';
color: #666;
font-style: italic;
}
/* スピナーアニメーション */
.spinner {
display: none;
width: 20px;
height: 20px;
border: 2px solid #f3f3f3;
border-top: 2px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 10px;
}
.htmx-request .spinner {
display: inline-block;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
html<!-- ローディングスピナー付きボタン -->
<button hx-get="/api/slow-operation" hx-target="#result">
データを取得
<span class="spinner"></span>
</button>
アニメーション効果
htmx は CSS トランジションと組み合わせることで、滑らかなアニメーション効果を実現できます。
フェードイン/アウト効果
css/* アニメーション用 CSS */
.fade-in {
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
.fade-in.htmx-added {
opacity: 1;
}
.fade-out {
opacity: 1;
transition: opacity 0.3s ease-in-out;
}
.fade-out.htmx-swapping {
opacity: 0;
}
.slide-down {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.slide-down.htmx-added {
max-height: 1000px;
}
html<!-- アニメーション付きの要素 -->
<div
id="animated-content"
class="fade-in fade-out"
hx-get="/api/content"
hx-trigger="load"
>
初期コンテンツ
</div>
<button
hx-get="/api/new-content"
hx-target="#animated-content"
hx-swap="innerHTML transition:true"
>
アニメーション付きで更新
</button>
実用的なトースト通知
javascript// トースト通知を表示する関数
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.innerHTML = `
<div style="
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 4px;
color: white;
font-weight: bold;
z-index: 1000;
transform: translateX(100%);
transition: transform 0.3s ease-in-out;
${type === 'success' ? 'background: #28a745;' : ''}
${type === 'error' ? 'background: #dc3545;' : ''}
${type === 'info' ? 'background: #007bff;' : ''}
">
${message}
</div>
`;
document.body.appendChild(toast);
// アニメーション開始
setTimeout(() => {
toast.firstElementChild.style.transform =
'translateX(0)';
}, 100);
// 3秒後に削除
setTimeout(() => {
toast.firstElementChild.style.transform =
'translateX(100%)';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// htmx イベントと連携
document.addEventListener(
'htmx:afterRequest',
function (event) {
if (event.detail.successful) {
showToast('操作が完了しました', 'success');
}
}
);
まとめ
htmx を使った次世代フロントエンド開発について、基礎から実践まで幅広くご紹介しました。
htmx の主な利点
学習コストの圧倒的な低さ:HTML の知識があれば、すぐに使い始めることができます。React や Vue.js のような複雑な概念を学ぶ必要がありません。
開発効率の向上:環境構築からアプリケーション完成まで、従来のフレームワークの 1/3 から 1/5 の時間で開発できます。
メンテナンス性:依存関係が少なく、セキュリティリスクや更新作業が大幅に軽減されます。
既存プロジェクトとの親和性:段階的な導入が可能で、既存の Web アプリケーションに簡単に組み込むことができます。
実践での活用場面
htmx は以下のような場面で特に威力を発揮します:
- 管理画面や内部ツールの開発
- プロトタイプや MVP の迅速な構築
- 既存の Web アプリケーションへの段階的な機能追加
- 学習コストを抑えたい小〜中規模チームでの開発
次のステップ
htmx をさらに活用するために、以下の学習を進めてみてください:
- サーバーサイドフレームワークとの統合(Rails、Django、Laravel など)
- WebSocket を使ったリアルタイム通信
- Progressive Web App (PWA) 対応
- テスト手法の習得
htmx は、Web 開発の複雑さに疲れた多くの開発者にとって、新鮮で効率的な選択肢となるでしょう。ぜひ実際のプロジェクトで試してみて、その魅力を体感してください!
関連リンク
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
- review
人類はなぜ地球を支配できた?『サピエンス全史 上巻』ユヴァル・ノア・ハラリが解き明かす驚愕の真実
- review
え?世界はこんなに良くなってた!『FACTFULNESS』ハンス・ロスリングが暴く 10 の思い込みの正体
- review
瞬時に答えが出る脳に変身!『ゼロ秒思考』赤羽雄二が贈る思考力爆上げトレーニング
- review
関西弁のゾウに人生変えられた!『夢をかなえるゾウ 1』水野敬也が教えてくれた成功の本質