Skip to content
Gratuit

Contactez-nous pour recevoir votre devis

Comment créer des blocs WordPress Gutenberg sur-mesure avec Tailwind CSS

Partager l'article


Le problème que personne ne résout vraiment

Tapez « créer un thème WordPress avec Tailwind CSS » sur Google. Vous trouverez des dizaines de tutoriels. Ils vous montrent comment installer Tailwind CSS, configurer PostCSS, lancer npm run watch, et appliquer quelques classes utilitaires sur votre header.php. Certains vont plus loin : ils intègrent Tailwind dans un starter comme Undercore_tw, configurent le purge, mettent en place des composants PHP.

Et puis le tutoriel s’arrête.

Parce que la vraie question — comment pousser Tailwind jusqu’à l’éditeur Gutenberg, jusqu’aux blocs sur-mesure, jusqu’au système de design complet disponible pour le rédacteur de contenu — n’a presque jamais de réponse satisfaisante.

Le fossé est réel. D’un côté, un thème Tailwind propre, performant, avec des breakpoints cohérents et une palette bien définie. De l’autre, des blocs Gutenberg qui utilisent leurs propres classes, leurs propres logiques d’espacement, et qui cassent visuellement la cohérence dès qu’un rédacteur touche à la mise en page.

C’est exactement ce problème qu’UFO Blocks essaie de résoudre : construire des blocs Gutenberg qui parlent nativement le langage Tailwind, où chaque contrôle dans le panneau latéral de l’éditeur se traduit directement en classe utilitaire dans le HTML final. Pas d’abstraction supplémentaire, pas de CSS custom parallèle — les mêmes classes Tailwind CSS que dans le reste du thème.

Ce guide explique comment c’est construit, et comment vous pouvez appliquer la même approche à vos propres blocs. Il est rédigé par UFO Agency, agence web basée à Pau spécialisée dans la création de sites internet WordPress sur-mesure.


1. Pourquoi utiliser Tailwind dans Gutenberg pour créer des sites internet WordPress ?

La question mérite d’être posée franchement : Gutenberg génère déjà du CSS, les thèmes en génèrent, les plugins en génèrent. Ajouter Tailwind, c’est choisir une couche utilitaire qui va à l’encontre du CSS-par-composant classique.

Chez UFO Agency, ce choix s’explique par trois raisons concrètes.

La cohérence avec le thème. Si votre thème est construit sur Tailwind CSS — via un starter comme Undercore_tw par exemple, qui offre une base WordPress + Tailwind sans surcharge — les blocs Gutenberg peuvent parler le même langage — mêmes espacements, mêmes breakpoints, même palette via les CSS variables du thème. Le rédacteur qui choisit un gap-8 dans l’éditeur obtiendra exactement le même 2rem que celui défini dans la charte graphique.

La rapidité de prototypage. Construire un bloc de grille responsive sans écrire une ligne de CSS custom, juste en composant des classes grid-cols-2, gap-4, md:col-span-2 — c’est une productivité mesurable sur des projets clients avec des délais serrés.

Le poids final. Tailwind avec son moteur JIT ne génère que le CSS réellement utilisé. Sur un site WordPress optimisé visant des scores PageSpeed proches de 100/100, c’est un avantage non négligeable.

Note : UFO Blocks ship un CSS Tailwind pré-compilé. Si votre thème intègre déjà Tailwind, vous pouvez désactiver ces feuilles de style pour éviter les doublons — nous y revenons en fin d’article.


2. Architecture du plugin

UFO Blocks expose deux blocs complémentaires qui fonctionnent ensemble : ufo-blocks/ufo-grid (le conteneur de grille CSS) et ufo-blocks/ufo-row (la cellule). Cette relation parent/enfant stricte est au cœur de l’architecture.

ufo-blocks/
├── ufo-blocks.php          ← Plugin entry point, enregistrement des blocs
├── package.json
├── webpack.config.js
├── src/
│   ├── ufo-grid/           ← Le conteneur de grille
│   │   ├── block.json      ← Métadonnées & attributs
│   │   ├── index.js        ← registerBlockType
│   │   ├── edit.js         ← Composant éditeur (React)
│   │   ├── save.js         ← Rendu front-end statique
│   │   ├── editor.css      ← Styles spécifiques à l'éditeur
│   │   └── style.css       ← CSS front (pas de Tailwind compilé ici)
│   ├── ufo-row/            ← La cellule de grille
│   │   └── ... (même structure)
│   └── icons.js            ← SVG icons pour les blocs
└── build/                  ← Généré par npm run build

Chaque bloc est un dossier autonome contenant son manifeste JSON, ses composants React pour l’éditeur, sa fonction de sauvegarde, et ses styles. Point important : le plugin ne compile aucune classe Tailwind. Les fichiers style.css des blocs ne contiennent que du CSS spécifique au rendu dans l’éditeur ou des ajustements visuels mineurs. Tout Tailwind est géré par le thème — c’est lui qui, via @source, scanne les fichiers JS du plugin et produit le CSS final.


3. block.json : le manifeste du bloc

Depuis WordPress 5.8 et le Block API v3, block.json est le fichier central d’un bloc. Il déclare le nom, la catégorie, les scripts à charger, et surtout les attributs — les données configurables depuis le panneau latéral de l’éditeur.

Voici le manifeste de ufo-grid :

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "ufo-blocks/ufo-grid",
  "version": "1.0.0",
  "title": "ufo Grid",
  "category": "ufo-blocks",
  "icon": "LayoutDashboard",
  "description": "Système de grille responsive utilisant Tailwind CSS",
  "allowedBlocks": ["ufo-blocks/ufo-row", "ufo-blocks/ufo-figure"],
  "keywords": ["grid", "layout", "columns", "tailwind", "ufo"],
  "textdomain": "ufo-blocks",
  "attributes": {
    "columnsCount": {
      "type": "number",
      "default": 2
    },
    "rowsCount": {
      "type": "number",
      "default": 0
    },
    "gap": {
      "type": "string",
      "default": "4",
      "enum": ["0", "1", "2", "3", "4", "5", "6", "7", "8"]
    },
    "verticalAlignment": {
      "type": "string"
    },
    "reverseMobile": {
      "type": "boolean",
      "default": false
    },
    "minHeight": {
      "type": "string",
      "default": ""
    },
    "borderRadius": {
      "type": "string",
      "default": ""
    },
    "isBoxed": {
      "type": "boolean",
      "default": false
    },
    "backgroundImage": {
      "type": "object",
      "default": {
        "url": "",
        "id": null,
        "alt": ""
      }
    },
    "backgroundSize": {
      "type": "string",
      "default": "cover"
    },
    "backgroundPosition": {
      "type": "string",
      "default": "center center"
    },
    "backgroundVideo": {
      "type": "object",
      "default": {
        "url": "",
        "id": null
      }
    },
    "paddingX": {
      "type": "string",
      "default": "0",
      "enum": ["0", "2", "4", "6", "8", "10", "12", "14", "16"]
    },
    "paddingY": {
      "type": "string",
      "default": "0",
      "enum": ["0", "2", "4", "6", "8", "10", "12", "14", "16"]
    }
  },
  "supports": {
    "align": ["wide", "full"],
    "html": false,
    "color": {
      "gradients": true,
      "background": true,
      "text": true
    },
    "spacing": {
      "margin": true,
      "padding": true,
      "blockGap": true
    },
    "__experimentalBorder": {
      "color": true,
      "radius": true,
      "style": true,
      "width": true
    },
    "anchor": true
  },
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css",
  "style": "file:./style-index.css"
}

Deux points importants à noter. Les attributs dont la valeur est une string de chiffre ("4", "0", "8") correspondent directement à des suffixes Tailwind : gap-4, px-0, py-8. Ce n’est pas un accident — la valeur de l’attribut est injectée directement dans le nom de la classe via une fonction utilitaire. C’est le principe central du système.

Utiliser apiVersion: 3 active les dernières optimisations Gutenberg dont l’iframing de l’éditeur, ce qui isole mieux vos styles et évite les fuites de CSS entre l’éditeur et la page.


4. index.js : enregistrement du bloc

Le fichier index.js est le point d’entrée du bloc côté JavaScript. Son rôle : importer les métadonnées JSON, les composants edit et save, et appeler registerBlockType.

import { registerBlockType } from '@wordpress/blocks';
import metadata from './block.json';
import Edit from './edit';
import save from './save';
import { gridIcon } from '../icons';

import './style.css';
import './editor.css';

registerBlockType( metadata.name, {
  ...metadata,
  icon: gridIcon,
  edit: Edit,
  save,
} );

L’import direct de block.json via le spread ...metadata permet à WordPress de lire les attributs, supports et métadonnées directement depuis le fichier JSON, sans dupliquer les déclarations en JavaScript. C’est la bonne pratique depuis le Block API v2.


5. edit.js : l’interface éditeur

C’est le composant React affiché dans l’éditeur Gutenberg. Il reçoit attributes et setAttributes en props, et il est responsable de deux choses : le rendu visuel du bloc dans l’éditeur, et les contrôles du panneau latéral via InspectorControls.

import { useBlockProps, InnerBlocks, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, RangeControl, ToggleControl, SelectControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { buildGridClasses } from './utils';

export default function Edit({ attributes, setAttributes }) {
  const { columnsCount, gap, paddingX, paddingY, isBoxed, reverseMobile } = attributes;

  // Génération dynamique des classes Tailwind depuis les attributs
  const gridClasses = buildGridClasses( attributes );

  const blockProps = useBlockProps({
    className: gridClasses,
  });

  return (
    <>
      <InspectorControls>

        <PanelBody title={ __( 'Grille', 'ufo-blocks' ) } initialOpen={ true }>
          <RangeControl
            label={ __( 'Colonnes', 'ufo-blocks' ) }
            value={ columnsCount }
            onChange={ ( val ) => setAttributes({ columnsCount: val }) }
            min={ 1 }
            max={ 12 }
          />
          <SelectControl
            label={ __( 'Gap', 'ufo-blocks' ) }
            value={ gap }
            options={[
              { label: 'Aucun', value: '0' },
              { label: 'XS',    value: '2' },
              { label: 'SM',    value: '4' },
              { label: 'MD',    value: '6' },
              { label: 'LG',    value: '8' },
            ]}
            onChange={ ( val ) => setAttributes({ gap: val }) }
          />
          <ToggleControl
            label={ __( 'Inverser sur mobile', 'ufo-blocks' ) }
            checked={ reverseMobile }
            onChange={ ( val ) => setAttributes({ reverseMobile: val }) }
          />
        </PanelBody>

      </InspectorControls>

      <div { ...blockProps }>
        <InnerBlocks
          allowedBlocks={ ['ufo-blocks/ufo-row'] }
          orientation="horizontal"
          renderAppender={ InnerBlocks.ButtonBlockAppender }
        />
      </div>
    </>
  );
}

La clé ici, c’est allowedBlocks sur InnerBlocks : il limite les blocs enfants autorisés à ufo-blocks/ufo-row uniquement. C’est ce qui garantit la cohérence de la grille — on ne peut pas glisser un bloc texte ou image directement dans le conteneur.


6. save.js : le rendu front-end

La fonction save produit le HTML qui sera sérialisé dans la base de données et rendu sur le front-end. Contrairement à edit, c’est une fonction pure : pas de state, pas d’effets, uniquement les attributs en entrée et du JSX en sortie.

import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
import { buildGridClasses, buildBackgroundStyle } from './utils';

export default function save({ attributes }) {
  const { backgroundVideo } = attributes;
  const gridClasses = buildGridClasses( attributes );
  const bgStyle     = buildBackgroundStyle( attributes );

  const blockProps = useBlockProps.save({
    className: gridClasses,
    style: bgStyle,
  });

  return (
    <div { ...blockProps }>

      { /* Vidéo de fond optionnelle */ }
      { backgroundVideo?.url && (
        <video
          className="absolute inset-0 w-full h-full object-cover -z-10"
          autoPlay muted loop playsInline
          src={ backgroundVideo.url }
          aria-hidden="true"
        />
      )}

      <InnerBlocks.Content />
    </div>
  );
}

⚠️ Attention à la Block Validation. Modifier save.js après avoir publié des contenus entraîne une Block Validation Error dans WordPress : le HTML sauvegardé ne correspond plus au rendu attendu. Si vous modifiez la structure de sortie, vous devez fournir une migration via le tableau deprecated du bloc.


7. InnerBlocks : composition parent/enfant

UFO Blocks exploite une relation parent/enfant stricte : ufo-grid est le conteneur, ufo-row est la cellule. Cette contrainte est déclarée dans le block.json de ufo-row via la propriété parent :

{
  "name": "ufo-blocks/ufo-row",
  "parent": ["ufo-blocks/ufo-grid"],
  "attributes": {
    "colSpan":  { "type": "string", "default": "" },
    "colStart": { "type": "string", "default": "" },
    "colEnd":   { "type": "string", "default": "" },
    "rowSpan":  { "type": "string", "default": "" },
    "rowStart": { "type": "string", "default": "" },
    "rowEnd":   { "type": "string", "default": "" },
    "paddingX": { "type": "string", "default": "0" },
    "paddingY": { "type": "string", "default": "0" },
    "backgroundImage": {
      "type": "object",
      "default": { "url": "", "id": 0, "alt": "" }
    }
  }
}

La propriété parent fait deux choses simultanément : elle empêche le bloc d’apparaître dans l’inserter global de Gutenberg, et elle le rend visible uniquement à l’intérieur d’un ufo-grid. Résultat : l’éditeur ne peut pas créer une cellule orpheline.

Côté edit.js, chaque ufo-row utilise InnerBlocks sans restriction d’allowedBlocks — ce qui permet d’y insérer n’importe quel bloc Gutenberg standard (texte, image, bouton, galerie, etc.) :

const blockProps = useBlockProps({
  className: buildRowClasses( attributes ),
});

return (
  <div { ...blockProps }>
    {/* Pas de restriction : tous les blocs WP sont acceptés dans la cellule */}
    <InnerBlocks renderAppender={ InnerBlocks.ButtonBlockAppender } />
  </div>
);

8. Le vrai cœur du système : @source dans le thème

C’est ici que l’approche d’UFO Blocks se distingue vraiment de ce que montrent la plupart des tutoriels.

Il n’y a aucune classe construite dynamiquement dans le plugin. Pas de concaténation de chaînes, pas de safelist à maintenir, pas de logique utilitaire qui assemble des noms de classes à la volée. Toutes les classes Tailwind sont écrites littéralement dans les fichiers source JavaScript du plugin — exactement comme vous les écririez dans un template PHP ou un composant React de votre thème.

Ce qui rend ça possible, c’est une seule ligne dans la feuille de style CSS du thème :

/**
 * On pointe Tailwind vers les fichiers source du plugin
 * pour qu'il scanne et inclue toutes leurs classes dans le build du thème.
 */
@source "../../../plugins/ufo-blocks/src/**/*.js";

Cette directive @source (Tailwind v4) indique à l’analyseur de Tailwind d’aller chercher les classes utilisées dans les fichiers JavaScript du plugin, en plus des fichiers du thème. Le résultat : un seul build CSS qui couvre à la fois le thème et les blocs Gutenberg.

Les classes dans les fichiers JS du plugin sont donc de simples strings statiques, lisibles et prévisibles :

// edit.js (ufo-grid) — extrait
const blockProps = useBlockProps({
  className: 'grid grid-cols-2 gap-4 px-0 py-0',
});

// edit.js (ufo-row) — extrait
const blockProps = useBlockProps({
  className: 'col-span-1 self-start px-4',
});

L’analyseur statique de Tailwind les détecte sans effort, comme il le ferait dans n’importe quel fichier source. Pas de configuration spéciale, pas de contournement.

Cette approche résout élégamment le problème évoqué en préambule : le design system du thème — sa palette, ses espacements, ses breakpoints — s’applique naturellement aux blocs Gutenberg via un build CSS unique et cohérent. Le rédacteur qui configure un bloc dans l’éditeur utilise les mêmes tokens visuels que le designer qui a construit le thème.


9. Build & workflow de développement

UFO Blocks s’appuie sur le toolchain standard WordPress (@wordpress/scripts), étendu pour traiter Tailwind via PostCSS.

// package.json
{
  "scripts": {
    "start":      "wp-scripts start",
    "build":      "wp-scripts build",
    "lint:js":    "wp-scripts lint-js",
    "lint:css":   "wp-scripts lint-style",
    "plugin-zip": "wp-scripts plugin-zip"
  },
  "devDependencies": {
    "@wordpress/scripts": "^30.0.0",
    "tailwindcss":        "^4.0.0",
    "postcss":            "^8.0.0",
    "autoprefixer":       "^10.0.0"
  }
}

Le plugin lui-même n’embarque pas de configuration Tailwind autonome. Tailwind est géré côté thème — c’est lui qui pilote le build CSS global, en incluant les sources du plugin via @source. Le plugin ne produit donc pas de feuille de style Tailwind indépendante en production quand il est couplé à un thème Tailwind.

Pour le développement au quotidien :

# Depuis le dossier du plugin
npm install

# Mode watch — recompile les scripts JS à chaque modification
npm start

# Build de production
npm run build

10. Éviter les conflits avec le thème

UFO Blocks ship son propre CSS compilé (ufo-grid-style et ufo-row-style) pour fonctionner indépendamment de tout thème. Mais si votre thème utilise Tailwind v4 et intègre déjà le @source vers le plugin, ce CSS devient redondant — voire source de conflits de spécificité.

Dans ce cas, désactivez les styles du plugin depuis le functions.php de votre thème :

// functions.php
add_action( 'wp_enqueue_scripts', function() {
    // Le thème gère Tailwind pour tout le site, y compris les blocs UFO
    wp_dequeue_style( 'ufo-grid-style' );
    wp_dequeue_style( 'ufo-row-style' );
}, 100 );

Et vérifiez que votre CSS de thème pointe bien vers les sources du plugin :

/* style.css ou app.css du thème */
@import "tailwindcss";

/* Sources du thème */
@source "./templates/**/*.php";
@source "./src/**/*.{js,jsx}";

/* Sources du plugin UFO Blocks */
@source "../../../plugins/ufo-blocks/src/**/*.js";

C’est cette ligne @source qui boucle le système. Tailwind CSS scanne les fichiers JS du plugin comme il scannerait n’importe quel fichier du thème, détecte les classes statiques, et les inclut dans le build CSS unique. Un seul fichier CSS pour gouverner l’ensemble — thème et blocs Gutenberg confondus. Couplé à un starter comme Undercore_tw, cette approche permet de démarrer un projet WordPress avec un design system cohérent de bout en bout, dès la première ligne de code.


UFO Blocks est un plugin open source sous licence GPL v2.
Contributions et issues bienvenues sur GitHub.


UFO Agency — Agence web spécialisée WordPress, PrestaShop & Next.js, basée à Pau.
ufo-agency.com · Création de sites internet