Skip to content

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

Partager

Pourquoi les tutoriels Tailwind s’arrêtent avant Gutenberg

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, lancer la compilation et appliquer quelques classes utilitaires sur votre header.php. Certains vont plus loin : ils partent d’un starter comme _tw ou TailPress et mettent en place des composants PHP.

Et puis le tutoriel s’arrête.

La question suivante reste presque toujours sans réponse : comment pousser Tailwind jusqu’à l’éditeur Gutenberg, jusqu’à des blocs sur mesure que le rédacteur règle depuis le panneau latéral ? Sans ça, le thème parle Tailwind avec ses espacements et sa palette, pendant que les blocs utilisent leurs propres classes et cassent la cohérence visuelle dès qu’un rédacteur touche à la mise en page.

Ce guide montre comment j’ai résolu ce problème avec UFO Blocks, un plugin open source que j’utilise sur mes projets WordPress : chaque réglage de l’éditeur devient une classe Tailwind dans le HTML final, compilée par le thème avec le reste du site. Le code présenté est celui de la version 1.6.1. Si vous découvrez Gutenberg, commencez plutôt par Pourquoi créer votre site WordPress avec Gutenberg.


1. Pourquoi utiliser Tailwind dans Gutenberg ?

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 aussi. Ajouter Tailwind, c’est choisir une couche utilitaire qui va à l’encontre du CSS écrit composant par composant. Trois raisons justifient ce choix.

La cohérence avec le thème. Si votre thème est construit sur Tailwind CSS, les blocs reprennent ses espacements, ses points de rupture et sa palette. Le rédacteur qui choisit un gap-8 dans l’éditeur obtient exactement les 2rem prévus par la charte graphique.

La rapidité de prototypage. Une grille responsive se construit en composant grid-cols-2, gap-4 ou md:col-span-2, sans écrire de CSS dédié. Sur des projets clients aux délais serrés, le gain de temps est net.

Le poids final. Tailwind ne génère que le CSS réellement utilisé. Sur un site WordPress qui vise des scores PageSpeed proches de 100, chaque kilo-octet évité compte.

UFO Blocks ne fournit d’ailleurs aucun CSS Tailwind : c’est le thème qui compile toutes les classes, celles des blocs comprises. J’y reviens en section 8.


2. Organiser les fichiers du plugin

UFO Blocks regroupe neuf blocs : la grille (ufo-grid) et sa cellule (ufo-row), un titre avec icône, des boutons, une FAQ avec ses données structurées, une icône et un bloc de date. Chaque bloc vit dans son propre dossier :

ufo-blocks/
├── ufo-blocks.php                    ← enregistre les 9 blocs
├── includes/
│   ├── class-ufo-blocks-updater.php  ← mises à jour depuis GitHub
│   ├── ufo-blocks-icons.php          ← icônes SVG pour les rendus PHP
│   └── ufo-faq-schema.php            ← données structurées FAQPage
├── src/
│   ├── ufo-grid/
│   │   ├── block.json
│   │   ├── index.js
│   │   ├── edit.js
│   │   ├── inspector.js              ← réglages du panneau latéral
│   │   ├── shared.js                 ← classes Tailwind, communes à edit et save
│   │   ├── save.js
│   │   ├── editor.css
│   │   └── style.css
│   ├── ufo-row/                      ← même organisation
│   ├── ufo-title/                    ← bloc dynamique, avec render.php
│   └── …                             ← faq, faq-item, button, buttons-group, icon, time
├── build/                            ← généré par wp-scripts
└── package.json

Le fichier shared.js joue un rôle clé : il contient les fonctions qui calculent les classes Tailwind à partir des réglages. edit.js et save.js l’importent tous les deux, ce qui garantit le même HTML dans l’éditeur et sur le site.

Côté PHP, le fichier principal enregistre chaque bloc à partir du dossier compilé. WordPress lit alors le block.json et charge les scripts et styles qu’il déclare :

function create_block_ufo_blocks_block_init()
{
    register_block_type(__DIR__ . "/build/ufo-grid");
    register_block_type(__DIR__ . "/build/ufo-row");
    register_block_type(__DIR__ . "/build/ufo-title");
    // … et les autres blocs
}
add_action("init", "create_block_ufo_blocks_block_init");

3. Déclarer le bloc dans block.json

Depuis WordPress 5.8, block.json est le fichier central d’un bloc. Il déclare le nom, la catégorie, les fichiers à charger et surtout les attributs, c’est-à-dire les données réglables depuis le panneau latéral. Voici un extrait de celui de ufo-grid :

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "ufo-blocks/ufo-grid",
  "title": "ufo Grid",
  "category": "ufo-blocks",
  "allowedBlocks": ["ufo-blocks/ufo-row", "core/image"],
  "attributes": {
    "columnsCount": { "type": "number", "default": 2 },
    "gap": {
      "type": "string",
      "default": "4",
      "enum": ["0", "1", "2", "3", "4", "5", "6", "7", "8"]
    },
    "paddingX": {
      "type": "string",
      "default": "0",
      "enum": ["0", "2", "4", "6", "8", "10", "12", "14", "16"]
    },
    "isBoxed": { "type": "boolean", "default": false }
  },
  "supports": {
    "align": ["wide", "full"],
    "html": false,
    "anchor": true
  },
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css",
  "style": "file:./style-index.css"
}

Les valeurs de gap ou paddingX sont des chaînes (« 4 », « 8 ») qui deviennent directement le suffixe d’une classe Tailwind : gap-4, px-8. L’enum limite les valeurs possibles, et c’est précisément ce qui rend la liste de classes de la section 8 finie et connue à l’avance.

"apiVersion": 3 (WordPress 6.3 et plus) rend le bloc compatible avec l’éditeur en iframe, qui isole mieux les styles de l’éditeur de ceux de l’administration. Et depuis WordPress 6.5, allowedBlocks peut se déclarer directement dans block.json.


4. Enregistrer le bloc avec index.js

Le fichier index.js est le point d’entrée JavaScript du bloc. Il importe les métadonnées, les composants edit et save, et appelle registerBlockType :

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

registerBlockType(metadata.name, {
    icon: getBlockIcon('LayoutDashboard', {
        background: '#F6F6F7',
        color: '#EB9188'
    }),
    edit: Edit,
    save,
});

Passer metadata.name suffit : comme le bloc est déjà enregistré côté PHP à partir de son block.json, WordPress connaît ses attributs et ses supports. Le JavaScript n’ajoute que ce que le JSON ne sait pas décrire, ici une icône personnalisée et les deux composants.


5. Construire l’interface d’édition avec edit.js

Les classes sont calculées dans shared.js, à partir des attributs :

// src/ufo-grid/shared.js (extrait)
export function getInnerClasses(attributes) {
  const { columnsCount, rowsCount, gap, reverseMobile, equalRows, isBoxed } =
    attributes;

  return [
    reverseMobile ? "md:grid" : "grid",
    "grid-cols-1",
    equalRows && "md:auto-rows-fr",
    gap !== undefined && `gap-${gap}`,
    columnsCount !== undefined && `md:grid-cols-${columnsCount}`,
    rowsCount && `grid-rows-${rowsCount}`,
    isBoxed && "max-w-content mx-auto w-full",
  ]
    .filter(Boolean)
    .join(" ");
}

Le composant d’édition s’appuie sur ces fonctions, avec useInnerBlocksProps pour accueillir les cellules de la grille. Les réglages du panneau latéral vivent dans un fichier à part, inspector.js, pour garder edit.js lisible (extrait simplifié) :

import { useBlockProps, useInnerBlocksProps } from "@wordpress/block-editor";
import Inspector from "./inspector";
import { getWrapperClasses, getInnerClasses, getWrapperStyle } from "./shared";
import "./editor.css";

export default function Edit({ attributes, setAttributes }) {
  const { columnsCount } = attributes;

  // Une cellule ufo-row par colonne à l'insertion du bloc
  const getTemplate = () =>
    Array(columnsCount).fill(0).map(() => ["ufo-blocks/ufo-row"]);

  const blockProps = useBlockProps({
    className: getWrapperClasses(attributes),
    style: getWrapperStyle(attributes),
  });

  const innerBlocksProps = useInnerBlocksProps(
    { className: getInnerClasses(attributes) },
    {
      template: getTemplate(),
      allowedBlocks: ["ufo-blocks/ufo-row", "core/image"],
      orientation: "horizontal",
      templateLock: false,
    },
  );

  return (
    <>
      <Inspector attributes={attributes} setAttributes={setAttributes} />
      <div {...blockProps}>
        <div {...innerBlocksProps} />
      </div>
    </>
  );
}

Trois réglages de useInnerBlocksProps font le travail. template crée une cellule par colonne à l’insertion du bloc. allowedBlocks n’accepte que des ufo-row et des images, ce qui protège la structure de la grille. orientation: "horizontal" adapte le déplacement des cellules dans l’éditeur.


6. Générer le HTML final avec save.js

La fonction save produit le HTML enregistré en base de données et affiché sur le site. Contrairement à edit, c’est une fonction pure : pas d’état, pas d’effets, uniquement les attributs en entrée et du JSX en sortie. Elle réutilise les mêmes fonctions de shared.js (extrait simplifié) :

import { useBlockProps, useInnerBlocksProps } from "@wordpress/block-editor";
import { getWrapperClasses, getInnerClasses, getWrapperStyle } from "./shared";

export default function save({ attributes }) {
  const blockProps = useBlockProps.save({
    className: getWrapperClasses(attributes),
    style: getWrapperStyle(attributes),
  });

  const innerBlocksProps = useInnerBlocksProps.save({
    className: getInnerClasses(attributes),
  });

  return (
    <div {...blockProps}>
      <div {...innerBlocksProps} />
    </div>
  );
}

⚠️ Attention à la validation des blocs. Modifier save.js après avoir publié des contenus provoque une erreur de validation dans l’éditeur : le HTML enregistré ne correspond plus à celui que la fonction produit. Pour changer la structure de sortie, il faut fournir une migration dans le tableau deprecated du bloc, ou passer le bloc en rendu PHP (section 11).


7. Imbriquer les blocs avec InnerBlocks

La relation entre la grille et ses cellules est déclarée dans le block.json de ufo-row, par la propriété parent :

{
  "name": "ufo-blocks/ufo-row",
  "parent": ["ufo-blocks/ufo-grid"],
  "attributes": {
    "colSpan": { "type": "string", "default": "" },
    "colStart": { "type": "string", "default": "" },
    "rowSpan": { "type": "string", "default": "" }
  }
}

La propriété parent fait deux choses : elle retire le bloc de l’inserteur général de Gutenberg, et elle ne le propose qu’à l’intérieur d’un ufo-grid. Le rédacteur ne peut pas créer de cellule orpheline.

Dans la cellule, aucune restriction : on y insère n’importe quel bloc (texte, image, bouton, galerie…). La cellule passe ses propres props à useInnerBlocksProps, ce qui évite un conteneur supplémentaire :

// src/ufo-row/edit.js (extrait)
const blockProps = useBlockProps({
  className: getTailwindClasses(attributes),
  style: getBlockStyle(attributes),
});

// Les props du bloc et celles des blocs enfants sont fusionnées :
// un seul <div> dans le HTML final, sans conteneur supplémentaire.
const innerBlocksProps = useInnerBlocksProps(blockProps, {
  templateLock: false,
});

return (
  <>
    <Inspector attributes={attributes} setAttributes={setAttributes} />
    <div {...innerBlocksProps} />
  </>
);

8. Déclarer les classes dynamiques avec @source inline()

Les classes de la grille sont construites à partir des attributs : getInnerClasses() assemble gap- et la valeur du réglage, md:grid-cols- et le nombre de colonnes. C’est pratique côté éditeur, mais Tailwind ne voit pas ces classes. Son analyseur lit les fichiers comme du texte et ne retient que des noms de classes complets : gap-4 écrit en clair est détecté, `gap-${gap}` ne l’est pas. Sans précaution, ces classes n’existent tout simplement pas dans le CSS final.

Tailwind v3 réglait ça avec une safelist dans tailwind.config.js. En v4, la configuration passe dans le CSS, et la directive @source inline() (Tailwind 4.1 et plus) prend le relais. Sa syntaxe à accolades génère toutes les variantes en une ligne. Voici ce que le thème déclare pour UFO Blocks :

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

/* Classes écrites en clair dans le plugin (JS et rendus PHP) */
@source "../../../plugins/ufo-blocks/src/**/*.js";
@source "../../../plugins/ufo-blocks/src/**/*.php";

/* Classes générées depuis les attributs des blocs */
@source inline("md:grid-cols-{1,2,3,4,5,6}");
@source inline("grid-rows-{1,2,3,4,5,6}");
@source inline("gap-{0,1,2,3,4,5,6,7,8}");
@source inline("px-{0,2,4,6,8,10,12,14,16}");
@source inline("py-{0,2,4,6,8,10,12,14,16}");
@source inline("md:col-span-{1,2,3,4,5,6}");
@source inline("md:col-start-{1,2,3,4,5,6,7}");
@source inline("md:col-end-{1,2,3,4,5,6,7,8}");
@source inline("row-span-{1,2,3,4,5,6}");
@source inline("row-start-{1,2,3,4,5,6,7}");
@source inline("row-end-{1,2,3,4,5,6,7}");
@source inline("h-{24,32,40,48,56,64,72,80,96,120,150}");
@source inline("rounded-{sm,md,lg,xl,2xl,3xl}");

Les deux premières directives font analyser les fichiers du plugin pour toutes les classes écrites en clair, y compris dans les rendus PHP. Les suivantes couvrent les classes générées : chaque ligne correspond à un réglage du bloc, gap-{0,…,8} aux valeurs de l’enum de la section 3, md:grid-cols-{1,…,6} aux bornes du réglage Colonnes.

Une seule règle à respecter : si vous élargissez la plage d’un réglage, élargissez la ligne correspondante. Sinon, la nouvelle valeur s’enregistre dans le bloc mais ne produit aucun style. Le coût reste faible : une centaine de classes, quelques kilo-octets avant compression, pour une grille entièrement réglable depuis l’éditeur.

Tailwind 4.1 accepte aussi les plages dans les accolades, ce qui raccourcit les listes régulières :

@source inline("gap-{0..8}");
@source inline("md:col-span-{1..6}");
@source inline("px-{0..16..2}");

9. Compiler et travailler au quotidien

UFO Blocks s’appuie sur l’outillage standard de WordPress, @wordpress/scripts. Le plugin n’embarque ni Tailwind ni PostCSS : il compile ses blocs, et le thème s’occupe du CSS.

// package.json (extrait)
"scripts": {
  "prebuild": "node scripts/generate-icon-svgs.js",
  "build": "wp-scripts build",
  "start": "wp-scripts start",
  "lint:js": "wp-scripts lint-js",
  "plugin-zip": "wp-scripts plugin-zip --config .distignore"
}

Le script prebuild génère avant chaque compilation les icônes SVG utilisées par les rendus PHP. Le fichier webpack.config.js complète la configuration par défaut pour retirer les console.* du code de production. Au quotidien, deux processus tournent en parallèle :

# Dans le dossier du plugin
npm install
npm start          # recompile les blocs à chaque modification

# Dans le dossier du thème, en parallèle
npm run watch      # recompile le CSS Tailwind (selon votre starter)

# Avant une mise en production
npm run build

10. Ce que le thème doit fournir

Puisque le plugin ne livre aucun CSS, le thème doit faire tourner Tailwind CSS v4 et déclarer les directives de la section 8. Sans elles, les blocs s’affichent sans mise en page. C’est un choix assumé : un seul fichier CSS pour le thème et les blocs, sans doublon ni conflit de spécificité entre deux compilations de Tailwind.

Pour que les blocs aient le même rendu dans l’éditeur et sur le site, chargez aussi ce CSS dans l’éditeur (add_editor_style() dans un thème classique, ou les styles du thème dans un thème FSE). Un starter comme _tw ou TailPress fournit déjà cette base.


11. Afficher du contenu dynamique avec render.php

Un bloc n’est pas obligé de stocker son HTML en base de données. Avec un rendu PHP, WordPress n’enregistre que les attributs, et le HTML est produit à chaque affichage. Le bloc ufo-title fonctionne ainsi. Il suffit de déclarer le fichier de rendu dans block.json (WordPress 6.1 et plus) :

{
  "apiVersion": 3,
  "name": "ufo-blocks/ufo-title",
  "editorScript": "file:./index.js",
  "render": "file:./render.php"
}

et de faire renvoyer null à la fonction save :

// src/ufo-title/save.js
// Rendu délégué à render.php : aucune validation HTML en base de données.
export default function save() {
  return null;
}

Le fichier render.php reçoit trois variables : $attributes, $content (le HTML éventuellement enregistré) et $block. Voici l’essentiel de celui de ufo-title :

<?php
// src/ufo-title/render.php (extrait)
// Variables fournies par WordPress : $attributes, $content, $block

$level           = (int) ( $attributes['level'] ?? 3 );
$heading_content = $attributes['content'] ?? '';

// Blocs publiés avant le passage en rendu PHP : le titre n'existe
// que dans le HTML sauvegardé, on le récupère.
if ( empty( $heading_content ) && ! empty( $content ) ) {
	preg_match( '/<h[1-6][^>]*>(.*?)<\/h[1-6]>/si', $content, $m );
	if ( ! empty( $m[1] ) ) {
		$heading_content = $m[1];
	}
}

$tag = 'h' . max( 1, min( 6, $level ) );
$wrapper_attrs = get_block_wrapper_attributes();
?>
<div <?php echo $wrapper_attrs; ?>>
	<div class="flex items-center not-prose">
		<<?php echo esc_html( $tag ); ?>><?php echo wp_kses_post( $heading_content ); ?></<?php echo esc_html( $tag ); ?>>
	</div>
</div>

Le bloc était statique à l’origine. Le passage en rendu PHP posait une question : que faire des titres déjà publiés, dont le texte n’existe que dans le HTML enregistré ? Le repli lit ce HTML quand l’attribut content est vide. Les anciens blocs s’affichent sans migration et sans erreur de validation. get_block_wrapper_attributes() ajoute les classes de typographie et de couleur choisies dans l’éditeur, et chaque sortie passe par une fonction d’échappement.

Je choisis le rendu PHP quand la structure HTML du bloc risque d’évoluer, ou quand il dépend de données du serveur. Une modification de render.php s’applique alors à tous les contenus d’un coup, sans erreur de validation. Je garde save.js pour les blocs stables comme la grille, dont le HTML reste lisible même si le plugin est désactivé. C’est aussi pour ça que le thème analyse les fichiers *.php du plugin : les classes des rendus PHP sont écrites en clair dans ces fichiers.


12. Maintenir le plugin dans la durée

Un plugin installé chez des clients doit pouvoir se mettre à jour proprement. UFO Blocks suit le versionnage sémantique, et chaque changement est documenté dans un CHANGELOG. À chaque tag vX.Y.Z, une action GitHub compile les blocs, vérifie l’archive ufo-blocks.zip et publie la version.

Les sites se mettent ensuite à jour depuis leur tableau de bord, comme pour n’importe quel plugin. L’en-tête Update URI empêche wordpress.org de proposer un plugin homonyme à la place, et une petite classe répond au filtre update_plugins_github.com avec la dernière version publiée :

// Plugin Name: Ufo Blocks
// Update URI:  https://github.com/Vince-ALIEN/ufo-blocks

La mise à jour automatique est désactivée sur une copie Git locale, pour ne jamais écraser un environnement de développement.


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

Questions fréquentes sur les blocs Gutenberg et Tailwind CSS

Peut-on utiliser Tailwind CSS avec Gutenberg sans conflits de styles ?

Oui, à condition de ne pas charger la remise à zéro de Tailwind (preflight) par-dessus les styles de l’éditeur, et de laisser le thème compiler seul les classes des blocs. Un plugin qui embarquerait son propre CSS Tailwind ferait doublon avec celui du thème : c’est pour ça qu’UFO Blocks n’en fournit aucun.

Faut-il connaître React pour créer des blocs Gutenberg ?

Une base en JavaScript suffit pour démarrer. Gutenberg utilise React pour l’interface éditeur, mais les composants courants (RichText, InspectorControls, useInnerBlocksProps) suivent des modèles répétitifs bien documentés. En quelques heures, vous créez un bloc fonctionnel.

Quel est l’avantage des blocs Gutenberg sur mesure par rapport à Elementor ?

Un bloc Gutenberg sur mesure génère du HTML propre, sans CSS ni JavaScript inutile. Elementor charge plusieurs centaines de kilo-octets de CSS et de JavaScript sur chaque page, même si vous n’utilisez qu’une fraction de ses fonctionnalités. Des blocs natifs produisent des pages plus légères, donc plus rapides.

Comment structurer un plugin pour gérer plusieurs blocs Gutenberg ?

Un plugin unique avec un dossier par bloc (src/mon-bloc/). Chaque bloc a son block.json, son index.js, son edit.js, et un save.js ou un render.php selon qu’il est statique ou dynamique. Un seul @wordpress/scripts compile l’ensemble, et le fichier principal du plugin enregistre chaque bloc avec register_block_type().

Les blocs Gutenberg sur mesure sont-ils stables lors des mises à jour WordPress ?

Oui, tant que vous utilisez les API officielles (@wordpress/blocks, @wordpress/block-editor) et que vous évitez les éléments internes non documentés. Le vrai risque vient de vos propres modifications : changer le HTML de save.js casse les blocs déjà publiés, sauf si vous fournissez une migration dans le tableau deprecated, ou si vous passez le bloc en rendu PHP.