htmx 2.0 propose une approche radicalement différente du développement web moderne : HTML enrichi au lieu de JavaScript lourd. En 2025, avec ses 14 KB et son approche hypermedia, htmx défie React, Vue et autres frameworks complexes en revenant aux fondamentaux du web.
🚀 htmx : La Philosophie
htmx permet de créer des applications web interactives modernes en utilisant principalement HTML avec des attributs, plutôt que JavaScript.
Hello World htmx
<!DOCTYPE html>
<html>
<head>
<title>htmx Demo</title>
<!-- htmx 2.0: seulement 14 KB ! -->
<script src="https://unpkg.com/htmx.org@2.0.0"></script>
</head>
<body>
<!-- Click sur le bouton fait une requête AJAX -->
<button
hx-get="/api/data"
hx-target="#result"
hx-swap="innerHTML">
Charger les données
</button>
<!-- Le résultat s'affiche ici -->
<div id="result"></div>
<!-- Pas de JavaScript nécessaire ! -->
</body>
</html>
Attributs htmx 2.0
<!-- hx-get: requête GET AJAX -->
<button hx-get="/users">Charger utilisateurs</button>
<!-- hx-post: requête POST -->
<form hx-post="/users" hx-target="#message">
<input name="name" required>
<button type="submit">Créer</button>
</form>
<!-- hx-put/hx-delete: autres verbes HTTP -->
<button hx-put="/users/123">Mettre à jour</button>
<button hx-delete="/users/123">Supprimer</button>
<!-- hx-trigger: événements personnalisés -->
<input
hx-get="/search"
hx-trigger="keyup changed delay:500ms"
hx-target="#results">
<!-- hx-swap: stratégies d'insertion -->
<div
hx-get="/content"
hx-swap="outerHTML">
<!-- Options: innerHTML, outerHTML, beforebegin, afterbegin, etc. -->
</div>
<!-- hx-select: extraire une partie de la réponse -->
<button
hx-get="/page"
hx-select="#main-content"
hx-target="#content">
Charger section
</button>
💡 Exemples Concrets htmx
Todo List interactive
<!-- HTML + htmx -->
<div id="todo-app">
<!-- Formulaire ajout todo -->
<form
hx-post="/todos"
hx-target="#todo-list"
hx-swap="beforeend"
hx-on::after-request="this.reset()">
<input name="text" required placeholder="Nouvelle tâche">
<button type="submit">Ajouter</button>
</form>
<!-- Liste des todos -->
<ul id="todo-list">
<!-- Généré par le serveur -->
</ul>
</div>
# Backend Python (Flask)
from flask import Flask, render_template_string
app = Flask(__name__)
todos = []
@app.route('/todos', methods=['POST'])
def add_todo():
text = request.form.get('text')
todo = {'id': len(todos) + 1, 'text': text, 'done': False}
todos.append(todo)
# Retourner seulement le HTML du nouveau todo
return render_template_string('''
<li id="todo-{{ todo.id }}">
<input type="checkbox"
hx-put="/todos/{{ todo.id }}/toggle"
hx-target="#todo-{{ todo.id }}"
hx-swap="outerHTML">
<span>{{ todo.text }}</span>
<button hx-delete="/todos/{{ todo.id }}"
hx-target="#todo-{{ todo.id }}"
hx-swap="outerHTML">
Supprimer
</button>
</li>
''', todo=todo)
@app.route('/todos/<int:id>/toggle', methods=['PUT'])
def toggle_todo(id):
todo = next((t for t in todos if t['id'] == id), None)
todo['done'] = not todo['done']
return render_template_string('''
<li id="todo-{{ todo.id }}" class="{{ 'done' if todo.done else '' }}">
<input type="checkbox" {{ 'checked' if todo.done else '' }}
hx-put="/todos/{{ todo.id }}/toggle"
hx-target="#todo-{{ todo.id }}"
hx-swap="outerHTML">
<span>{{ todo.text }}</span>
<button hx-delete="/todos/{{ todo.id }}"
hx-target="#todo-{{ todo.id }}"
hx-swap="outerHTML">
Supprimer
</button>
</li>
''', todo=todo)
@app.route('/todos/<int:id>', methods=['DELETE'])
def delete_todo(id):
global todos
todos = [t for t in todos if t['id'] != id]
return '' # Retour vide pour supprimer l'élément
Infinite scroll
<!-- Liste avec pagination infinie -->
<div id="users-list">
<!-- Utilisateurs chargés initialement -->
<div class="user">User 1</div>
<div class="user">User 2</div>
<!-- ... -->
<!-- Trigger pour charger plus -->
<div
hx-get="/users?page=2"
hx-trigger="intersect once"
hx-target="#users-list"
hx-swap="beforeend">
<div class="loading">Chargement...</div>
</div>
</div>
<!-- Backend retourne plus d'utilisateurs + nouveau trigger -->
Formulaire avec validation
<!-- Formulaire avec validation live -->
<form hx-post="/register" hx-target="#result">
<div>
<label>Email:</label>
<input
name="email"
type="email"
hx-post="/validate/email"
hx-trigger="blur"
hx-target="next .error"
hx-swap="innerHTML">
<span class="error"></span>
</div>
<div>
<label>Mot de passe:</label>
<input
name="password"
type="password"
hx-post="/validate/password"
hx-trigger="blur"
hx-target="next .error">
<span class="error"></span>
</div>
<button type="submit">S'inscrire</button>
</form>
<div id="result"></div>
// Backend Node.js (Express)
app.post('/validate/email', (req, res) => {
const { email } = req.body;
if (!email.includes('@')) {
return res.send('<span class="error">Email invalide</span>');
}
// Vérifier si email existe déjà
const exists = users.some(u => u.email === email);
if (exists) {
return res.send('<span class="error">Email déjà utilisé</span>');
}
res.send('<span class="success">✓ Email valide</span>');
});
app.post('/validate/password', (req, res) => {
const { password } = req.body;
if (password.length moins de 8) {
return res.send('<span class="error">Minimum 8 caractères</span>');
}
res.send('<span class="success">✓ Mot de passe valide</span>');
});
⚡ Fonctionnalités Avancées htmx 2.0
WebSockets support
<!-- Connection WebSocket -->
<div hx-ws="connect:/ws">
<!-- Envoyer un message -->
<form hx-ws="send">
<input name="message">
<button>Envoyer</button>
</form>
<!-- Les messages reçus s'affichent ici -->
<div id="messages"></div>
</div>
# Backend WebSocket (Python avec FastAPI)
from fastapi import FastAPI, WebSocket
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
data = await websocket.receive_text()
# Broadcast message to all clients
message_html = f'''
<div class="message">
<strong>User:</strong> {data}
</div>
'''
await websocket.send_text(message_html)
Server-Sent Events (SSE)
<!-- Real-time updates avec SSE -->
<div
hx-ext="sse"
sse-connect="/events"
sse-swap="message"
hx-target="#notifications"
hx-swap="beforeend">
</div>
<div id="notifications"></div>
// Backend SSE (Node.js)
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Envoyer un événement toutes les 5 secondes
const interval = setInterval(() => {
const html = `
<div class="notification">
Nouveau message à ${new Date().toLocaleTimeString()}
</div>
`;
res.write(`event: message\ndata: ${html}\n\n`);
}, 5000);
req.on('close', () => {
clearInterval(interval);
});
});
Extensions htmx
<!-- htmx extensions pour fonctionnalités avancées -->
<script src="https://unpkg.com/htmx.org/dist/ext/json-enc.js"></script>
<script src="https://unpkg.com/htmx.org/dist/ext/debug.js"></script>
<!-- Envoyer JSON au lieu de form-data -->
<form
hx-post="/api/users"
hx-ext="json-enc">
<input name="name">
<input name="email">
<button>Submit</button>
</form>
<!-- Debug mode -->
<div hx-get="/data" hx-ext="debug"></div>
📊 htmx vs React : Comparaison
Même feature, approches différentes
React approach
// TodoApp.tsx - React
import { useState } from 'react';
interface Todo {
id: number;
text: string;
done: boolean;
}
export function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
const [input, setInput] = useState('');
const addTodo = async () => {
const response = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: input })
});
const newTodo = await response.json();
setTodos([...todos, newTodo]);
setInput('');
};
const toggleTodo = async (id: number) => {
await fetch(`/api/todos/${id}/toggle`, { method: 'PUT' });
setTodos(todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
));
};
const deleteTodo = async (id: number) => {
await fetch(`/api/todos/${id}`, { method: 'DELETE' });
setTodos(todos.filter(t => t.id !== id));
};
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => deleteTodo(todo.id)}>
Delete
</button>
</li>
))}
</ul>
</div>
);
}
// Nécessite: React, TypeScript, bundler, etc.
// Bundle size: ~150 KB (React + ReactDOM)
htmx approach
<!-- HTML + htmx (code vu précédemment) -->
<!-- Bundle size: 14 KB (htmx seulement) -->
<!-- Pas de build step, pas de framework -->
<!-- Le serveur gère l'état -->
Comparaison détaillée
| Aspect | React | htmx |
|---|---|---|
| Bundle size | 150+ KB | 14 KB |
| Build time | 5-30s | 0s (pas de build) |
| Courbe apprentissage | Élevée | Faible |
| Complexité | Haute | Basse |
| État client | ✅ Riche | ⚠️ Limité |
| Performance initiale | ⚠️ Hydration | ✅ Instantané |
| SEO | ⚠️ SSR requis | ✅ Natif |
| Offline | ✅ Possible | ⚠️ Limité |
| Écosystème | ✅✅✅ Énorme | ⚠️ Jeune |
🎯 Cas d'Usage Idéaux pour htmx
Dashboard admin
<!-- Dashboard avec stats temps réel -->
<div class="dashboard">
<!-- Stats auto-refresh toutes les 5s -->
<div
hx-get="/stats"
hx-trigger="every 5s"
hx-swap="innerHTML">
Chargement des stats...
</div>
<!-- Table utilisateurs avec pagination -->
<table>
<thead>...</thead>
<tbody
hx-get="/users?page=1"
hx-trigger="load"
hx-target="this"
hx-swap="innerHTML">
</tbody>
</table>
<!-- Actions rapides -->
<button
hx-post="/export"
hx-target="#result">
Exporter données
</button>
</div>
E-commerce checkout
<!-- Panier e-commerce -->
<div id="cart">
<!-- Items panier -->
<div class="cart-item">
<span>Produit A</span>
<input
type="number"
value="1"
hx-put="/cart/update/1"
hx-trigger="change"
hx-target="#cart"
hx-swap="outerHTML">
<button
hx-delete="/cart/1"
hx-target="closest .cart-item"
hx-swap="outerHTML">
Supprimer
</button>
</div>
<!-- Total mis à jour automatiquement -->
<div class="total">Total: 49.99€</div>
<!-- Checkout -->
<button
hx-get="/checkout"
hx-target="body"
hx-swap="innerHTML">
Commander
</button>
</div>
CMS / Content editing
<!-- Édition inline -->
<div
hx-get="/content/article-123"
hx-trigger="load"
hx-target="this"
hx-swap="innerHTML">
<!-- Contenu éditable -->
<div
contenteditable="true"
hx-put="/content/article-123"
hx-trigger="blur"
hx-include="this">
Contenu de l'article...
</div>
</div>
🚨 Limitations htmx
Quand NE PAS utiliser htmx
❌ **Application très interactive côté client**
- Jeux web
- Éditeurs complexes (type Figma)
- Applications temps réel collaboratives
❌ **Offline-first applications**
- PWA avec fonctionnement hors ligne
- Apps mobiles hybrides
❌ **Animations complexes**
- Transitions page sophistiquées
- UI avec beaucoup d'animations
❌ **État client très riche**
- Apps avec logique métier complexe client-side
- Calculs côté client intensifs
✅ **Mais htmx excelle pour**:
- CRUD applications
- Dashboards admin
- E-commerce
- CMS
- Formulaires interactifs
- Apps server-side first
💰 Coûts et Performance
Coût de développement
**Projet e-commerce typique**:
React + Next.js:
- Setup: 2-3 jours
- Développement: 4-6 semaines
- Maintenance: complexe
- Équipe: développeurs React expérimentés
htmx + Backend framework:
- Setup: quelques heures
- Développement: 2-3 semaines
- Maintenance: simple
- Équipe: développeurs backend standards
**Économie**: ~40% temps de développement
Performance web
**Metrics Web Vitals (site e-commerce)**:
React SPA:
- FCP: 2.1s
- LCP: 3.8s
- TTI: 4.2s
- Bundle: 450 KB
htmx:
- FCP: 0.6s
- LCP: 1.2s
- TTI: 0.8s
- Bundle: 14 KB
**Amélioration**: 3-4x plus rapide
🔮 Écosystème et Futur
Stacks populaires avec htmx (2025)
**BETH Stack**:
- Bun (runtime)
- Elysia (framework)
- Turso (database)
- htmx
**Python Stack**:
- FastAPI / Flask
- SQLAlchemy
- htmx
- Jinja2 templates
**Go Stack**:
- Gin / Echo
- Templ (templating)
- htmx
- PostgreSQL
**Rails Stack**:
- Ruby on Rails 8
- Hotwire + htmx
- PostgreSQL
Adoption 2025
**Statistiques**:
- htmx downloads: 2M+/mois
- GitHub stars: 35K+
- Croissance: +150% en 2024
**Adoptions notables**:
- GitHub (parties du site)
- Basecamp (Hotwire = htmx-like)
- Nombreuses startups B2B
🎓 Conclusion
htmx 2.0 propose une alternative rafraîchissante au JavaScript lourd des SPAs modernes. En revenant aux fondamentaux HTML avec l'interactivité moderne, htmx permet de construire des applications web performantes et maintenables avec une fraction de la complexité.
Utilisez htmx si :
- Application server-side first
- CRUD / admin / dashboard
- Équipe backend-focused
- Simplicité et performance prioritaires
- Budget/temps limités
Restez sur React/Vue si :
- UI très interactive côté client
- Application offline-first
- Équipe frontend experte
- Écosystème riche requis
L'avenir : htmx ne remplacera pas React, mais offre une voie alternative légitime pour de nombreux cas d'usage. En 2025, choisir htmx est un choix pragmatique et moderne, pas un retour en arrière. C'est le retour aux fondamentaux du web, en mieux.
Articles connexes
Pour approfondir le sujet, consultez également ces articles :




