Skip to Content

Chapitre 7.5 — Tailwind dans Moodle, honnêtement

Partie 7 : Frontend — Chapitre 5/6 Public : dev React/Next.js habitué à Tailwind. On répond franchement : peut-on utiliser Tailwind dans Moodle, et à quel prix ? Version de référence : Moodle 5.2 (Bootstrap 5.3 global, Tailwind v4 côté outillage).

⏱️ TL;DR

  • Moodle charge Bootstrap 5.3 globalement, sur toutes les pages. Tailwind et Bootstrap peuvent coexister, mais deux dangers : le preflight de Tailwind (reset CSS agressif) casserait le style Bootstrap de tout le site, et les collisions de noms de classes utilitaires.
  • Solution : Tailwind v4 sans preflight, préfixé (tw:) et scopé sous une classe racine (.tw-scope), de sorte que Tailwind n’agisse que dans votre composant et ne fuie pas dans le reste de Moodle.
  • On compile le CSS Tailwind dans le build du plugin (esbuild du ch. 7.4, ou un postcss/tailwind CLI), et on l’inclut soit via le CSS du plugin, soit injecté par le composant.
  • Le content de Tailwind doit pointer vers vos .tsx/.mustache pour que le tree-shaking ne supprime pas vos classes.
  • Pour un plugin isolé (un composant React), Tailwind scopé est viable et confortable. Pour thémer tout Moodle, Tailwind est le mauvais outil : le thème (Partie 8) est en SCSS + variables Bootstrap ; combattre Bootstrap sur tout le site avec Tailwind est une guerre perdue.
  • Verdict : Tailwind oui, dans un îlot scopé (composant/plugin) ; non comme système de design global à la place de Bootstrap.

🎯 Objectifs

  • Comprendre pourquoi Tailwind naïf casse Moodle (preflight, global Bootstrap).
  • Configurer Tailwind v4 sans preflight, préfixé et scopé.
  • Intégrer la compilation Tailwind au build du plugin.
  • Résoudre les conflits de classes Bootstrap/Tailwind.
  • Décider quand Tailwind est pertinent (îlot) et quand il ne l’est pas (thème global).

1. Le décor : Bootstrap 5 partout

Le thème Boost (défaut de Moodle 5.2) charge Bootstrap 5.3 globalement : ses classes (btn, card, row, d-flex, text-muted…) et surtout son reset/normalize s’appliquent à toutes les pages. Tout le HTML de Moodle — le vôtre inclus — vit dans cet environnement Bootstrap.

Tailwind, lui, est conçu pour être le framework CSS d’une app. Le poser tel quel sur Moodle crée deux collisions.

1.1 Danger n°1 : le preflight

Tailwind injecte par défaut un preflight : un reset CSS global qui remet à zéro marges, list-style, styles de titres, bordures de tableaux, etc. Sur une app Tailwind, c’est voulu. Dans Moodle, c’est catastrophique : le preflight écraserait le normalize de Bootstrap sur tout le site, cassant l’apparence de pages que vous n’avez jamais touchées (listes sans puces, titres délavés, formulaires dénudés…).

1.2 Danger n°2 : collisions de noms

Tailwind et Bootstrap partagent des noms de classes utilitaires proches (hidden, flex, border, p-2, text-center…) mais aux définitions différentes. Sans préfixe, une classe Tailwind peut redéfinir — ou être redéfinie par — une classe Bootstrap, avec un résultat imprévisible selon l’ordre de chargement du CSS.

⚠️ Piège n°1 (le plus grave) : ajouter un CSS Tailwind avec preflight à un plugin Moodle dégrade tout le site, pas seulement votre page — car le CSS est global. Un client verra ses cours « cassés » après l’installation de votre plugin. Le preflight doit être désactivé, sans exception.


2. La stratégie : Tailwind scopé, sans preflight, préfixé

L’objectif : Tailwind agit uniquement à l’intérieur de votre composant et ne fuit jamais dans le reste de Moodle. Trois mesures cumulatives.

  1. Désactiver le preflight — pas de reset global.
  2. Préfixer toutes les classes Tailwind (tw:flex au lieu de flex) — plus de collision de noms avec Bootstrap.
  3. Scoper sous une classe racine (.tw-scope) — les styles ne s’appliquent que sous ce conteneur.

2.1 Configuration Tailwind v4

Tailwind v4 se configure principalement en CSS (via @import et directives), avec des options par le plugin PostCSS. Le fichier CSS d’entrée de votre plugin :

/* public/local/reactdemo/react/styles.css */ /* Import de Tailwind SANS preflight : on n'importe que les couches utilities/components. */ @layer theme, base, components, utilities; @import "tailwindcss/theme.css" layer(theme); /* On N'IMPORTE PAS "tailwindcss/preflight.css" → pas de reset global. */ @import "tailwindcss/utilities.css" layer(utilities); /* Préfixe et scope : configurés via la directive @config ou les options PostCSS (v4). */

En Tailwind v4, on active le préfixe et on restreint le scope via la configuration du plugin (@tailwindcss/postcss) et une racine. Concrètement, on veut que la sortie ressemble à :

/* Résultat souhaité (simplifié) : chaque utilitaire est préfixé ET scopé. */ .tw-scope .tw\:flex { display: flex; } .tw-scope .tw\:p-4 { padding: 1rem; } .tw-scope .tw\:text-lg{ font-size: 1.125rem; }

Deux leviers pour y parvenir :

  • Préfixe tw: — pour que tw:flex ne heurte jamais le flex de Bootstrap.
  • Scope .tw-scope — obtenu soit par une option de Tailwind (important/racine), soit en post-traitant le CSS pour préfixer chaque sélecteur par .tw-scope (via un plugin PostCSS comme postcss-prefix-selector). Le scoping garantit que même un utilitaire préfixé ne s’applique que sous votre conteneur.

⚠️ Piège de version : la mécanique exacte (noms d’options, syntaxe @config vs config JS) diffère entre Tailwind v3 et v4, et v4 évolue vite. Vérifiez la doc Tailwind de la version que vous installez pour la syntaxe du préfixe et du scoping. Le principe (sans preflight + préfixe + scope) reste constant ; seule l’écriture change.

2.2 Le content (tree-shaking)

Tailwind ne génère que les classes qu’il voit dans vos sources. Il faut donc lui indiquer où chercher :

// tailwind.config (ou équivalent v4) export default { content: [ './react/**/*.{ts,tsx}', // vos composants React './templates/**/*.mustache', // vos templates Mustache ], corePlugins: { preflight: false }, // désactive le reset (v3) ; en v4, via l'import CSS prefix: 'tw:', };

⚠️ Piège du tree-shaking : si content ne couvre pas un fichier (ex. une classe construite dynamiquement `tw:text-${color}`), Tailwind ne génère pas la classe et le style manque en prod. Évitez les noms de classes concaténés dynamiquement, ou déclarez-les en safelist. Testez en build de prod (minifié), pas seulement en watch.


3. Intégration au build du plugin

On greffe la compilation Tailwind sur la toolchain esbuild du chapitre 7.4. Deux approches.

Approche 1 — CSS séparé, chargé par le plugin. On compile styles.css (Tailwind) en un CSS statique inclus via styles.css du plugin (chargé par Moodle) ou injecté par le composant. Simple, mais le CSS est global (d’où l’importance absolue du scope .tw-scope).

Approche 2 — CSS injecté par esbuild dans le bundle. esbuild peut importer le CSS et l’injecter au runtime quand le composant se monte. Le CSS ne charge que sur les pages qui utilisent le composant.

// esbuild.mjs — ajout du traitement CSS (extrait) import esbuild from 'esbuild'; import tailwind from '@tailwindcss/postcss'; import postcss from 'postcss'; // Plugin esbuild qui passe le CSS importé dans PostCSS + Tailwind. const tailwindPlugin = { name: 'tailwind', setup(build) { build.onLoad({filter: /\.css$/}, async (args) => { const fs = await import('node:fs/promises'); const source = await fs.readFile(args.path, 'utf8'); const result = await postcss([tailwind()]).process(source, {from: args.path}); return {contents: result.css, loader: 'text'}; // le composant injectera ce texte }); }, };

Puis, dans le composant, on injecte le CSS une fois :

// react/index.tsx (extrait) — injection idempotente du CSS scopé import css from './styles.css'; // texte CSS compilé par esbuild+tailwind function ensureStyles(): void { if (document.getElementById('local-reactdemo-tw')) { return; // déjà injecté } const style = document.createElement('style'); style.id = 'local-reactdemo-tw'; style.textContent = css; document.head.appendChild(style); } export function init(elementId: string, props: InitProps): void { ensureStyles(); const el = document.getElementById(elementId); if (!el) return; el.classList.add('tw-scope'); // active le scope Tailwind sur la racine createRoot(el).render(<App {...props} />); }

⚠️ Piège de l’injection multiple : sans le garde getElementById(...), chaque instance du composant réinjecterait le même bloc <style> → doublons dans le <head>. Injectez une seule fois (id unique de la balise style), comme ci-dessus.


4. Écrire du Tailwind scopé dans les composants

Avec le préfixe tw: et le scope .tw-scope (posé sur la racine par init), vos composants utilisent Tailwind normalement — en préfixant :

export function App({initialTasks}: AppProps) { return ( <div className="tw:flex tw:flex-col tw:gap-3 tw:p-4"> <h2 className="tw:text-xl tw:font-semibold">Mes tâches</h2> <ul className="tw:space-y-2"> {initialTasks.map((t) => ( <li key={t.id} className="tw:flex tw:items-center tw:justify-between tw:rounded tw:border tw:p-2"> <span className={t.completed ? 'tw:line-through tw:text-gray-400' : ''}>{t.name}</span> </li> ))} </ul> </div> ); }

Comme la racine porte .tw-scope et les classes sont préfixées, rien de ce Tailwind ne s’applique hors de ce composant, et il ne heurte pas Bootstrap.

💡 Pour un dev React : le préfixe tw: est un léger inconfort (votre muscle mémoire tape flex, pas tw:flex), mais c’est le prix de la coexistence pacifique avec un Bootstrap global que vous ne contrôlez pas. Vous pouvez configurer votre éditeur (extension Tailwind IntelliSense) pour reconnaître le préfixe. Alternative si le préfixe vous rebute : compter uniquement sur le scope .tw-scope + important — mais le préfixe reste la protection la plus robuste contre les collisions.


5. Résoudre les conflits résiduels

Même scopé et préfixé, quelques frictions subsistent :

  • Spécificité : un style Bootstrap très spécifique peut l’emporter sur un utilitaire Tailwind à spécificité égale. Le scope .tw-scope .tw\:x augmente la spécificité de Tailwind (deux classes), ce qui aide ; en dernier recours, l’option important de Tailwind (scopée !) force la priorité — à utiliser avec parcimonie.
  • Héritage : sans preflight, certaines propriétés héritées de Bootstrap (couleur de texte, font-family) « traversent » dans votre composant. C’est souvent souhaitable (cohérence visuelle avec Moodle) ; si ce ne l’est pas, redéclarez explicitement dans .tw-scope.
  • Composants Bootstrap mélangés : évitez de mélanger btn btn-primary (Bootstrap) et des utilitaires Tailwind conflictuels sur le même élément. Choisissez, par élément, un système.

⚠️ Piège : ne désactivez jamais le scope en pensant « je ne mets Tailwind que sur ma page ». Le CSS est global : un utilitaire Tailwind non scopé nommé tw:hidden peut ne pas heurter Bootstrap, mais un jour vous ajouterez une classe qui collisionnera. Le scope .tw-scope` est votre garantie structurelle, pas une optimisation optionnelle.


6. Le verdict : îlot oui, thème global non

  • Îlot (composant React, un plugin) : Tailwind scopé est viable et agréable. Vous gardez votre productivité Tailwind sur votre surface, sans nuire au reste. C’est le seul usage recommandé.
  • Design global (thémer Moodle) : Tailwind est le mauvais outil. Moodle se thème avec SCSS + les variables Bootstrap (Partie 8) : c’est le système que le core, les thèmes du marché et l’app mobile comprennent. Réécrire l’UI de Moodle en Tailwind reviendrait à désactiver Bootstrap sur tout le site (impossible sans casser les milliers de composants du core et des plugins tiers) ou à combattre Bootstrap classe par classe — une dette infinie. Pour une refonte « SaaS moderne », on personnalise Boost via SCSS, on ne le remplace pas par Tailwind.

💡 Pour un dev React : c’est le même arbitrage que le chapitre React (7.4), transposé au CSS. Tailwind, comme React, est un invité ponctuel dans un système server-first stylé par Bootstrap. Il brille dans un îlot que vous possédez entièrement ; il devient une source de souffrance dès qu’il prétend gouverner une surface que Bootstrap gouverne déjà. Le bon réflexe Moodle : s’intégrer à Bootstrap (utiliser ses classes, personnaliser ses variables) plutôt que de le remplacer.


7. Récapitulatif de la recette

MesurePourquoiComment
Pas de preflightne pas casser le style global de Moodlene pas importer preflight.css (v4) / corePlugins.preflight = false (v3)
Préfixe tw:éviter les collisions de noms avec Bootstrapoption prefix
Scope .tw-scopeconfiner Tailwind à votre composantracine + post-traitement du CSS / important scopé
content completne pas perdre de classes au tree-shakingpointer .tsx et .mustache
Injection unique du CSSpas de doublons <style>garde par id dans ensureStyles()
Îlot uniquementTailwind n’est pas un système de thème globalthème = SCSS + variables Bootstrap (Partie 8)

✏️ Exercices

Exercice 1 — Le site casse après installation. Un client installe votre plugin React/Tailwind et signale que tous ses cours ont perdu leurs puces de liste et leurs styles de titres. Cause et correctif immédiat ?

✅ Solution

Le preflight de Tailwind (reset CSS global) a été inclus dans le CSS du plugin, or ce CSS est global : il a écrasé le normalize de Bootstrap sur tout le site. Correctif immédiat : retirer l’import du preflight (ne pas importer tailwindcss/preflight.css en v4, ou corePlugins.preflight = false en v3), recompiler, redéployer. Vérifier ensuite que le reste de la recette (préfixe + scope) est en place pour éviter d’autres fuites.

Exercice 2 — Collision hidden. Une classe hidden se comporte différemment selon les pages. Expliquez et donnez la parade structurelle.

✅ Solution

hidden existe à la fois dans Bootstrap (via ses utilitaires) et Tailwind, avec des définitions/priorités différentes ; sans préfixe, laquelle gagne dépend de l’ordre de chargement du CSS → comportement imprévisible. Parade : préfixer Tailwind (tw:hidden) pour qu’il ne partage aucun nom avec Bootstrap, et scoper sous .tw-scope. Les deux systèmes cohabitent alors sans se recouvrir.

Exercice 3 — Classe manquante en prod. En watch tout marche, mais en build de prod un bouton perd son tw:bg-brand. La classe est construite ainsi : `tw:bg-${theme}`. Pourquoi et comment corriger ?

✅ Solution

Tailwind génère les classes qu’il voit littéralement dans les fichiers du content. Une classe concaténée dynamiquement (`tw:bg-${theme}`) n’apparaît nulle part en toutes lettres → tree-shakée → absente du CSS de prod. Correctifs : écrire les classes complètes (theme === 'brand' ? 'tw:bg-brand' : 'tw:bg-gray'), ou ajouter les variantes à une safelist. Toujours tester en build de prod (minifié), pas seulement en watch, pour attraper ces disparitions.

Exercice 4 — Thémer tout Moodle en Tailwind ? Un client demande de « refaire toute l’interface de Moodle en Tailwind pour un look moderne ». Que répondez-vous ?

✅ Solution

Que c’est le mauvais outil pour cet objectif. Moodle est stylé globalement par Bootstrap 5.3, et des milliers de composants du core et des plugins tiers en dépendent. Remplacer Bootstrap par Tailwind impliquerait soit de le désactiver (impossible sans tout casser), soit de le combattre classe par classe (dette infinie, non maintenable à chaque upgrade). La bonne approche pour un look moderne : personnaliser le thème Boost via SCSS et les variables Bootstrap (Partie 8) — couleurs, fontes, radius, composants — qui atteignent 100 % de l’UI proprement. Tailwind reste réservé à des îlots (composants React spécifiques).

Exercice 5 — Injection dupliquée. Avec deux instances de votre composant sur une page, le <head> contient deux blocs <style> identiques. Impact et correctif ?

✅ Solution

Impact : CSS dupliqué (poids inutile, et risque de confusion au débogage). Correctif : rendre l’injection idempotente — avant d’ajouter le <style>, vérifier son absence par un id unique (document.getElementById('local-reactdemo-tw')), et ne l’insérer que s’il manque. ensureStyles() est ainsi appelée par chaque init() mais n’agit qu’une fois.


🧠 Quiz de révision

Q1. Quels sont les deux dangers d’un Tailwind naïf dans Moodle ?

Réponse

(1) Le preflight (reset CSS global de Tailwind) écrase le style Bootstrap de tout le site, car le CSS est global. (2) Les collisions de noms de classes utilitaires entre Tailwind et Bootstrap, à la priorité imprévisible.

Q2. Quelles sont les trois mesures cumulatives pour un Tailwind sûr ?

Réponse

Désactiver le preflight (pas de reset global), préfixer les classes (tw:) pour éviter les collisions, et scoper sous une racine (.tw-scope) pour confiner les styles à votre composant.

Q3. Pourquoi faut-il inclure les .mustache dans le content de Tailwind ?

Réponse

Tailwind ne génère que les classes qu’il trouve dans les fichiers listés dans content. Si vos templates Mustache utilisent des classes Tailwind non présentes dans les .tsx, il faut les inclure, sinon ces classes sont tree-shakées et manquent en production.

Q4. Pourquoi Tailwind est-il inadapté pour thémer tout Moodle ?

Réponse

Parce que Moodle est stylé globalement par Bootstrap 5.3, dont dépendent des milliers de composants du core et des plugins. Remplacer/combattre Bootstrap avec Tailwind sur tout le site est impossible sans tout casser ou créer une dette infinie. Le thème se fait en SCSS + variables Bootstrap (Partie 8). Tailwind est réservé aux îlots.

Q5. Comment évite-t-on de dupliquer le CSS injecté avec plusieurs instances ?

Réponse

En rendant l’injection idempotente : la fonction ensureStyles() vérifie la présence d’une balise <style> par son id unique avant de l’ajouter, de sorte qu’un seul bloc de CSS existe quel que soit le nombre d’instances montées.

Chapitre suivant : 06 — core/reactive et UI avancée — la bibliothèque réactive interne de Moodle (StateManager, mutations, watchers, components) qui propulse l’éditeur de cours, quand l’utiliser plutôt que React, le drag & drop du core, les key events, l’accessibilité JS (ARIA, focus dans les modales) et la performance frontend. La troisième voie entre vanilla et React.