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

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

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 ejs

Creez la structure du projet :

mkdir -p views/partials public/css

Votre 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 endpoint
  • hx-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-list
  • x-data="{ showForm: false }" — etat local Alpine.js pour le toggle du formulaire
  • x-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.js

Ouvrez http://localhost:3000 dans votre navigateur. Vous devriez voir :

  1. Une barre de recherche — tapez n'importe quoi et les resultats se filtrent instantanement
  2. Un bouton "Nouvelle Tache" — affiche/masque le formulaire avec Alpine.js (aucune requete reseau)
  3. Des cartes de taches avec les actions Demarrer/Terminer/Modifier/Supprimer
  4. Trois colonnes — A Faire, En Cours, Termine

Etape 8 : Comprendre la repartition htmx / Alpine.js

Voici la cle architecturale :

ResponsabiliteOutilPourquoi
Recuperation de donneeshtmxLe serveur possede les donnees, renvoie du HTML
Soumission de formulairehtmxLe serveur valide et renvoie l'interface mise a jour
Recherche/filtragehtmxLe serveur filtre les donnees, renvoie un fragment HTML
Afficher/masquerAlpine.jsEtat purement UI, pas besoin de serveur
Validation de formulaireAlpine.jsRetour instantane, pas d'aller-retour
Menus deroulantsAlpine.jsInteraction locale uniquement
AnimationsCSS + Alpine.jsx-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

  1. Recherche : Tapez dans la barre de recherche — les taches doivent se filtrer en temps reel sans rechargement
  2. Creation : Cliquez sur "Nouvelle Tache", remplissez le formulaire, soumettez — la nouvelle tache doit apparaitre dans la colonne "A Faire"
  3. Changement de statut : Cliquez sur "Demarrer" sur une tache todo — elle doit se deplacer vers "En Cours"
  4. Edition en ligne : Cliquez sur "Modifier", changez le titre, appuyez sur Entree — le titre doit se mettre a jour sur place
  5. Suppression : Cliquez sur "Supprimer", confirmez — la carte de tache doit disparaitre avec une transition fluide
  6. 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 :

Metriquehtmx + Alpine.jsSPA React
Bundle JS~18 Ko (gzippe)~150-300 Ko
Time to Interactive~200ms~1-3s
Etape de build requiseNonOui
Rendu serveurNatifNecessite un setup SSR
SEO friendlyOui, par defautNecessite Next.js/Remix

Prochaines etapes

  • Explorez les extensions htmx comme response-targets et loading-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/persist et @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.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Creer un Podcast a partir d'un PDF avec Vercel AI SDK et LangChain.

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

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.

30 min read·