htmx et Alpine.js : Construire des Applications Web Interactives Sans Framework JavaScript Lourd

Fatigue d'envoyer des megaoctets de JavaScript a vos utilisateurs ? htmx et Alpine.js vous permettent de construire des applications web hautement interactives en utilisant des attributs HTML et un minimum de JavaScript. Dans ce tutoriel, vous allez construire un gestionnaire de taches en temps reel avec recherche, edition en ligne et transitions fluides — le tout sans bundler ni etape de build.
Objectifs d'apprentissage
A la fin de ce tutoriel, vous serez capable de :
- Comprendre l'approche hypermedia avec htmx
- Ajouter de l'interactivite cote client avec Alpine.js
- Construire des operations CRUD sans ecrire d'appels fetch
- Combiner htmx et Alpine.js pour une stack puissante et legere
- Deployer une application complete de gestionnaire de taches
Prerequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 18+ installe (nous utiliserons Express comme backend)
- Des connaissances de base en HTML et CSS
- Une familiarite avec les concepts des API REST
- Un editeur de code (VS Code recommande)
Aucune connaissance de React, Vue ou Angular n'est requise — c'est tout l'interet !
Ce que vous allez construire
Un gestionnaire de taches entierement fonctionnel avec :
- Recherche en temps reel via htmx
- Edition en ligne des taches sans rechargement de page
- Changement de statut avec Alpine.js
- Transitions CSS fluides lors des echanges de contenu
- Rendu cote serveur avec des reponses HTML partielles
Etape 1 : Comprendre l'approche Hypermedia
Les SPA traditionnelles fonctionnent ainsi : le navigateur telecharge un bundle JavaScript, qui recupere ensuite du JSON depuis une API et affiche l'interface cote client. htmx inverse ce modele — le serveur renvoie des fragments HTML, et htmx les insere dans le DOM.
SPA traditionnelle :
Navigateur → Bundle JS → Fetch JSON → Rendu DOM
Approche htmx :
Navigateur → Clic/Saisie → htmx envoie la requete → Serveur renvoie du HTML → htmx met a jour le DOM
Cela signifie :
- Moins de JavaScript envoye au client
- Pas de gestion d'etat cote client necessaire
- Le serveur controle l'interface — utilisez n'importe quel langage backend
- Amelioration progressive — fonctionne partiellement sans JS
Alpine.js complete htmx en gerant l'etat local de l'interface — menus deroulants, modales, toggles — des choses qui ne necessitent pas d'aller-retour serveur.
Etape 2 : Configuration du projet
Creez un nouveau repertoire de projet et initialisez-le :
mkdir htmx-task-manager && cd htmx-task-manager
npm init -y
npm install express ejsCreez la structure du projet :
mkdir -p views/partials public/cssVotre repertoire devrait ressembler a ceci :
htmx-task-manager/
├── views/
│ ├── partials/
│ │ ├── task-list.ejs
│ │ ├── task-item.ejs
│ │ └── task-form.ejs
│ └── index.ejs
├── public/
│ └── css/
│ └── styles.css
├── server.js
└── package.json
Etape 3 : Configuration du serveur Express
Creez server.js avec la logique backend :
const express = require("express");
const app = express();
app.set("view engine", "ejs");
app.use(express.static("public"));
app.use(express.urlencoded({ extended: true }));
// Stockage des taches en memoire
let tasks = [
{ id: 1, title: "Apprendre les bases de htmx", status: "done", priority: "high" },
{ id: 2, title: "Explorer Alpine.js", status: "in-progress", priority: "medium" },
{ id: 3, title: "Construire le gestionnaire de taches", status: "todo", priority: "high" },
{ id: 4, title: "Ajouter la recherche", status: "todo", priority: "low" },
];
let nextId = 5;
// Page principale
app.get("/", (req, res) => {
res.render("index", { tasks });
});
// Recherche de taches — renvoie un fragment HTML
app.get("/tasks/search", (req, res) => {
const query = req.query.q?.toLowerCase() || "";
const filtered = tasks.filter((t) =>
t.title.toLowerCase().includes(query)
);
res.render("partials/task-list", { tasks: filtered });
});
// Creer une tache — renvoie le HTML de la nouvelle tache
app.post("/tasks", (req, res) => {
const task = {
id: nextId++,
title: req.body.title,
status: "todo",
priority: req.body.priority || "medium",
};
tasks.push(task);
res.render("partials/task-item", { task });
});
// Mettre a jour le statut d'une tache
app.patch("/tasks/:id/status", (req, res) => {
const task = tasks.find((t) => t.id === parseInt(req.params.id));
if (!task) return res.status(404).send("Tache non trouvee");
task.status = req.body.status;
res.render("partials/task-item", { task });
});
// Modifier le titre d'une tache (en ligne)
app.put("/tasks/:id", (req, res) => {
const task = tasks.find((t) => t.id === parseInt(req.params.id));
if (!task) return res.status(404).send("Tache non trouvee");
task.title = req.body.title;
res.render("partials/task-item", { task });
});
// Supprimer une tache
app.delete("/tasks/:id", (req, res) => {
tasks = tasks.filter((t) => t.id !== parseInt(req.params.id));
res.send("");
});
app.listen(3000, () => {
console.log("Serveur lance sur http://localhost:3000");
});Remarquez comment chaque endpoint renvoie du HTML, pas du JSON. C'est le principe fondamental de htmx.
Etape 4 : Le layout principal avec htmx et Alpine.js
Creez views/index.ejs :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gestionnaire de Taches — htmx + Alpine.js</title>
<!-- htmx depuis le CDN — c'est tout, pas d'etape de build -->
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<!-- Alpine.js depuis le CDN -->
<script defer src="https://unpkg.com/alpinejs@3.14.8/dist/cdn.min.js"></script>
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body>
<div class="container" x-data="{ showForm: false }">
<header>
<h1>Gestionnaire de Taches</h1>
<p>Construit avec htmx + Alpine.js — zero bundler, zero framework</p>
</header>
<!-- Barre de recherche — htmx envoie une requete a chaque frappe -->
<div class="search-bar">
<input
type="search"
name="q"
placeholder="Rechercher des taches..."
hx-get="/tasks/search"
hx-trigger="input changed delay:300ms, search"
hx-target="#task-list"
hx-indicator=".search-spinner"
/>
<span class="search-spinner htmx-indicator">Recherche...</span>
</div>
<!-- Basculer le formulaire avec Alpine.js (pas de serveur necessaire) -->
<button @click="showForm = !showForm" class="btn-primary">
<span x-text="showForm ? 'Annuler' : 'Nouvelle Tache'"></span>
</button>
<!-- Formulaire de nouvelle tache -->
<div x-show="showForm" x-transition class="task-form">
<%- include('partials/task-form') %>
</div>
<!-- Conteneur de la liste des taches -->
<div id="task-list">
<%- include('partials/task-list', { tasks }) %>
</div>
</div>
</body>
</html>Decomposons ce qui se passe :
hx-get="/tasks/search"— htmx envoie une requete GET a cet endpointhx-trigger="input changed delay:300ms"— se declenche 300ms apres que l'utilisateur arrete de taper (avec debounce)hx-target="#task-list"— le HTML de la reponse remplace le contenu de#task-listx-data="{ showForm: false }"— etat local Alpine.js pour le toggle du formulairex-show="showForm"— affiche conditionnellement le formulaire, sans aller-retour serveur
Etape 5 : Creation des fragments (partials)
Liste des taches (views/partials/task-list.ejs)
<div class="task-columns">
<% const statuses = ['todo', 'in-progress', 'done']; %>
<% statuses.forEach(status => { %>
<div class="column">
<h2 class="column-header column-<%= status %>">
<%= status === 'todo' ? 'A Faire' : status === 'in-progress' ? 'En Cours' : 'Termine' %>
<span class="count">
(<%= tasks.filter(t => t.status === status).length %>)
</span>
</h2>
<div class="task-items">
<% tasks.filter(t => t.status === status).forEach(task => { %>
<%- include('task-item', { task }) %>
<% }) %>
</div>
</div>
<% }) %>
</div>Element de tache (views/partials/task-item.ejs)
<div
class="task-card priority-<%= task.priority %>"
id="task-<%= task.id %>"
x-data="{ editing: false }"
>
<!-- Mode affichage -->
<div x-show="!editing">
<div class="task-header">
<span class="task-title"><%= task.title %></span>
<span class="priority-badge"><%= task.priority %></span>
</div>
<div class="task-actions">
<!-- Changement de statut avec htmx -->
<% if (task.status === 'todo') { %>
<button
hx-patch="/tasks/<%= task.id %>/status"
hx-vals='{"status": "in-progress"}'
hx-target="#task-list"
hx-get="/tasks/search?q="
hx-swap="innerHTML"
class="btn-sm btn-start"
>Demarrer</button>
<% } else if (task.status === 'in-progress') { %>
<button
hx-patch="/tasks/<%= task.id %>/status"
hx-vals='{"status": "done"}'
hx-target="#task-list"
hx-get="/tasks/search?q="
hx-swap="innerHTML"
class="btn-sm btn-done"
>Terminer</button>
<% } %>
<!-- Edition en ligne avec Alpine.js -->
<button @click="editing = true" class="btn-sm btn-edit">Modifier</button>
<!-- Suppression avec htmx -->
<button
hx-delete="/tasks/<%= task.id %>"
hx-target="#task-<%= task.id %>"
hx-swap="outerHTML"
hx-confirm="Supprimer cette tache ?"
class="btn-sm btn-delete"
>Supprimer</button>
</div>
</div>
<!-- Mode edition — Alpine gere la visibilite, htmx sauvegarde -->
<div x-show="editing" x-transition>
<form
hx-put="/tasks/<%= task.id %>"
hx-target="#task-<%= task.id %>"
hx-swap="outerHTML"
>
<input
type="text"
name="title"
value="<%= task.title %>"
class="edit-input"
@keydown.escape="editing = false"
/>
<div class="edit-actions">
<button type="submit" class="btn-sm btn-save">Enregistrer</button>
<button type="button" @click="editing = false" class="btn-sm">Annuler</button>
</div>
</form>
</div>
</div>Formulaire de tache (views/partials/task-form.ejs)
<form
hx-post="/tasks"
hx-target="#task-list"
hx-get="/tasks/search?q="
hx-swap="innerHTML"
x-data="{ title: '' }"
>
<div class="form-group">
<input
type="text"
name="title"
placeholder="Que faut-il faire ?"
x-model="title"
required
/>
<select name="priority">
<option value="low">Faible</option>
<option value="medium" selected>Moyenne</option>
<option value="high">Haute</option>
</select>
<button type="submit" class="btn-primary" :disabled="!title.trim()">
Ajouter
</button>
</div>
</form>Etape 6 : Ajout des styles et transitions
Creez public/css/styles.css :
:root {
--bg: #0f172a;
--surface: #1e293b;
--border: #334155;
--text: #e2e8f0;
--text-muted: #94a3b8;
--primary: #3b82f6;
--success: #22c55e;
--warning: #f59e0b;
--danger: #ef4444;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
}
.container {
max-width: 1100px;
margin: 0 auto;
padding: 2rem;
}
header { text-align: center; margin-bottom: 2rem; }
header h1 { font-size: 2rem; }
header p { color: var(--text-muted); }
.search-bar {
position: relative;
margin-bottom: 1.5rem;
}
.search-bar input {
width: 100%;
padding: 0.75rem 1rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 1rem;
}
.task-columns {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
margin-top: 1.5rem;
}
.task-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem;
margin-top: 0.75rem;
transition: all 0.2s ease;
}
.task-card:hover { border-color: var(--primary); }
/* Transitions htmx */
.htmx-swapping { opacity: 0; transition: opacity 0.2s ease-out; }
.htmx-settling { opacity: 1; transition: opacity 0.3s ease-in; }
@media (max-width: 768px) {
.task-columns { grid-template-columns: 1fr; }
}Etape 7 : Lancer l'application
Demarrez le serveur :
node server.jsOuvrez http://localhost:3000 dans votre navigateur. Vous devriez voir :
- Une barre de recherche — tapez n'importe quoi et les resultats se filtrent instantanement
- Un bouton "Nouvelle Tache" — affiche/masque le formulaire avec Alpine.js (aucune requete reseau)
- Des cartes de taches avec les actions Demarrer/Terminer/Modifier/Supprimer
- Trois colonnes — A Faire, En Cours, Termine
Etape 8 : Comprendre la repartition htmx / Alpine.js
Voici la cle architecturale :
| Responsabilite | Outil | Pourquoi |
|---|---|---|
| Recuperation de donnees | htmx | Le serveur possede les donnees, renvoie du HTML |
| Soumission de formulaire | htmx | Le serveur valide et renvoie l'interface mise a jour |
| Recherche/filtrage | htmx | Le serveur filtre les donnees, renvoie un fragment HTML |
| Afficher/masquer | Alpine.js | Etat purement UI, pas besoin de serveur |
| Validation de formulaire | Alpine.js | Retour instantane, pas d'aller-retour |
| Menus deroulants | Alpine.js | Interaction locale uniquement |
| Animations | CSS + Alpine.js | x-transition pour une UI fluide |
Regle generale : Si cela necessite des donnees du serveur, utilisez htmx. Si c'est purement visuel/interactif, utilisez Alpine.js.
Etape 9 : Patterns avances
Scroll infini avec htmx
Ajoutez la pagination a votre liste de taches :
<div
hx-get="/tasks?page=2"
hx-trigger="revealed"
hx-swap="afterend"
>
Chargement de plus de taches...
</div>Le trigger revealed se declenche quand l'element entre dans le viewport — parfait pour le scroll infini.
UI optimiste avec Alpine.js
Montrez un retour immediat avant la reponse du serveur :
<button
x-data="{ loading: false }"
@click="loading = true"
:class="loading && 'opacity-50'"
hx-delete="/tasks/1"
hx-on::after-request="loading = false"
>
<span x-show="!loading">Supprimer</span>
<span x-show="loading">Suppression...</span>
</button>Integration WebSocket
htmx supporte les connexions WebSocket pour les mises a jour en temps reel :
<div hx-ext="ws" ws-connect="/ws">
<div id="notifications" hx-swap-oob="beforeend">
<!-- Le serveur envoie du HTML ici -->
</div>
</div>Swaps Out-of-Band
Mettez a jour plusieurs parties de la page depuis une seule reponse :
<!-- La reponse du serveur peut inclure des elements out-of-band -->
<div id="task-list" hx-swap-oob="true">
<!-- Liste des taches mise a jour -->
</div>
<div id="task-count" hx-swap-oob="true">
<!-- Badge de compteur mis a jour -->
</div>Etape 10 : Ajouter un toggle theme sombre/clair
C'est un cas d'utilisation parfait pour Alpine.js — pas de serveur implique :
<div x-data="{ dark: true }" :class="dark ? 'theme-dark' : 'theme-light'">
<button @click="dark = !dark; localStorage.setItem('theme', dark ? 'dark' : 'light')">
<span x-text="dark ? 'Mode Clair' : 'Mode Sombre'"></span>
</button>
</div>Alpine.js gere le toggle, le CSS gere le style, et localStorage persiste la preference — le tout sans toucher au serveur.
Quand NE PAS utiliser htmx
htmx n'est pas le bon choix pour chaque projet :
- Etat client complexe — Si votre application doit gerer un etat profondement imbrique et interconnecte (comme un tableur ou un outil de design), un framework comme React est plus adapte
- Applications offline-first — htmx necessite une connexion serveur pour chaque interaction
- Collaboration temps reel intensive — Des outils comme Google Docs necessitent une resolution de conflits sophistiquee que htmx ne fournit pas
- Applications mobiles — Utilisez React Native, Flutter ou les SDKs natifs
Tester votre implementation
- Recherche : Tapez dans la barre de recherche — les taches doivent se filtrer en temps reel sans rechargement
- Creation : Cliquez sur "Nouvelle Tache", remplissez le formulaire, soumettez — la nouvelle tache doit apparaitre dans la colonne "A Faire"
- Changement de statut : Cliquez sur "Demarrer" sur une tache todo — elle doit se deplacer vers "En Cours"
- Edition en ligne : Cliquez sur "Modifier", changez le titre, appuyez sur Entree — le titre doit se mettre a jour sur place
- Suppression : Cliquez sur "Supprimer", confirmez — la carte de tache doit disparaitre avec une transition fluide
- Toggle formulaire : Cliquez sur "Nouvelle Tache" / "Annuler" — le formulaire doit coulisser sans aucune requete reseau
Depannage
Les requetes htmx ne se declenchent pas
Assurez-vous que le script htmx.org est charge avant vos elements HTML. Verifiez la console du navigateur pour les erreurs 404 sur l'URL du CDN.
Les directives Alpine.js ne fonctionnent pas
Assurez-vous que l'attribut defer est present sur la balise script d'Alpine.js. Alpine doit s'initialiser apres que le DOM soit pret.
Les reponses partielles remplacent toute la page
Verifiez votre attribut hx-target — il doit pointer vers un ID d'element specifique. S'il est omis, htmx remplace l'element qui a declenche la requete.
Comparaison de performances
Voici ce qu'une application htmx + Alpine.js typique envoie par rapport a une SPA React :
| Metrique | htmx + Alpine.js | SPA React |
|---|---|---|
| Bundle JS | ~18 Ko (gzippe) | ~150-300 Ko |
| Time to Interactive | ~200ms | ~1-3s |
| Etape de build requise | Non | Oui |
| Rendu serveur | Natif | Necessite un setup SSR |
| SEO friendly | Oui, par defaut | Necessite Next.js/Remix |
Prochaines etapes
- Explorez les extensions htmx comme
response-targetsetloading-states - Ajoutez htmx + SSE pour des notifications en temps reel
- Essayez de combiner htmx avec un backend Python (Django, Flask, FastAPI)
- Decouvrez les plugins Alpine.js comme
@alpinejs/persistet@alpinejs/intersect - Lisez le livre Hypermedia Systems pour une comprehension approfondie
Conclusion
htmx et Alpine.js representent un retour a la simplicite du developpement web rendu cote serveur — mais avec l'interactivite que les utilisateurs attendent des applications modernes. En laissant le serveur gerer les donnees et l'etat tout en utilisant un minimum de JavaScript pour les interactions UI, vous obtenez des applications plus rapides, plus simples a maintenir et plus accessibles.
La combinaison est particulierement puissante pour :
- Les applications CRUD (tableaux de bord admin, gestionnaires de taches, CMS)
- Les sites riches en contenu qui ont besoin d'un peu d'interactivite
- Les prototypes et MVP ou la vitesse de developpement compte
- Les equipes sans developpeurs frontend dedies
Essayez-le sur votre prochain projet — vous pourriez etre surpris de ce que vous pouvez accomplir sans framework JavaScript.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Créez votre première extension Chrome alimentée par l'IA avec Manifest V3 et OpenAI
Apprenez à créer une extension Chrome qui résume les pages web et explique du texte grâce à l'IA — étape par étape avec Manifest V3 et l'API OpenAI.

Construire des API REST avec Go et Fiber : Guide pratique pour débutants
Apprenez à construire des API REST rapides et prêtes pour la production avec Go et le framework Fiber. Ce guide pas à pas couvre la configuration du projet, le routage, le traitement JSON, la connexion à la base de données avec GORM, les middlewares, la gestion des erreurs et les tests — de zéro à une API fonctionnelle.

Serie de Tutoriels Laravel : Parcours d'Apprentissage Complet pour Developpeurs PHP
Votre guide complet pour apprendre Laravel 11. Suivez notre parcours d'apprentissage structure, des bases de PHP aux fonctionnalites avancees comme l'integration Stripe et les recommandations IA.