Accueil / Articles

Pourquoi les éditeurs visuels ont du mal avec les composants propriétaires de shadcn UI

Les éditeurs visuels ont du mal avec Shadcn UI car les cartes CVA se trouvent dans votre dépôt et Tailwind nécessite une compilation. La collecte des classes DOM permet de relier le mode conception dans React.

Éditeur : REACTAPP.TOP9 min de lecture2063 mots

Sources: shadcn/ui says the code is yours. So why can’t anything edit it?

shadcn/ui se présente comme une solution permettant de copier-coller du code sans souci : il suffit d’exécuter la CLI, de placer le fichier components/ui/button.tsx dans le répertoire, et de traiter ce fichier comme du code propriétaire. Il n’y a ni mécanisme d’obligation de mise à jour, ni conflit avec des packages externes, ni compétition avec des feuilles de style provenant d’autres sources.

Cette présentation soulève une question pratique : si le composant est déjà présent dans le projet, pourquoi faut-il encore l’ouvrir manuellement pour le modifier ?

La réponse est technique, et ce n’est pas la première supposition que font la plupart des équipes.

Les outils de création visuelle attendent un catalogue de propriétés. Les cartes CVA propriétaires sont différentes.

Les outils de création visuelle pour React supposent généralement que les composants proviennent d’un package disposant d’une interface de propriétés stable et documentée. Material UI en est l’exemple typique : un bouton expose des enums tels que variant, size et color. Un outil peut lire ces types, afficher des listes déroulantes et écrire à nouveau des chaînes de caractères. Le composant reste une boîte noire dotée d’un panneau de contrôle étiqueté à l’avant.

Ouvrez plutôt un bouton de shadcn.

const buttonVariants = cva( "inline-flex items-center justify-center rounded-md text-sm font-medium ...", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground ...", outline: "border border-input bg-background hover:bg-accent ...", }, size: { default: "h-10 px-4 py-2", sm: "h-9 px-3", lg: "h-11 px-8" }, }, defaultVariants: { variant: "default", size: "default" }, } )

Il n’y a pas de catalogue externe de propriétés ; il existe simplement une carte des variantes, écrite en TypeScript pur dans un fichier appartenant au projet.

On considère généralement que cela est moins avantageux pour les outils : rien à analyser dans node_modules, pas de site de documentation à extraire, et chaque fork évolue indépendamment. Les éditeurs conçus pour les packages ne peuvent vraiment pas viser ce format.

La lecture la plus pertinente va dans le sens opposé. Une carte CVA contient plus d’informations qu’une table de correspondance, et ces informations sont déjà enregistrées sur le disque.

Une table de correspondance indique que variant accepte la valeur "contained". Elle ne précise pas ce que représente "contained". Cette correspondance est générée à l’intérieur de la bibliothèque. Pour la connaître, il faut lire la documentation, deviner ou l’essayer directement.

La carte CVA indique que variant: "destructive" est bg-destructive text-destructive-foreground. La relation entre ces éléments fait partie des données sources sous contrôle local. Rien n’est caché, car rien n’a été stocké séparément.

Ainsi, l’affirmation est précise :

L’édition visuelle structurée a besoin de savoir ce qu’un contrôle fait, et non seulement qu’il existe. Les composants packagés cachent ces informations dans des mécanismes internes compilés. shadcn, quant à lui, les conserve sous forme d’objets littéraux dans le dépôt. Selon ce critère, shadcn devrait être la bibliothèque la plus facile à modifier visuellement — et non la plus difficile.

Ce qui manque encore dans le marché, c’est une édition structurée des variantes pilotée par votre propre carte CVA dans votre propre arborescence. Il existe des outils de visualisation Tailwind ainsi que des éditeurs de thèmes shadcn, mais ces outils réécrivent les variables CSS. Cela modifie ce à quoi se résout bg-primary ; cela ne change pas quelle variante utilise un bouton ni quels outils cette variante contient. L’obstacle pour la deuxième tâche se trouve à un niveau inférieur par rapport aux variantes.

Le véritable obstacle : les classes d’outils ne deviennent CSS qu’après analyse

MUI applique les styles en temps de exécution. Un objet sx={{ p: 2 }} se transforme en CSS au moment où le composant est rendu — il n’y a rien à précalculer, rien à scanner. Exécuter MUI dans un navigateur sans installation locale reste difficile, mais cette difficulté vient de la résolution des modules, des instances uniques de thème et d’émotion, ainsi que des versions cohérentes des paquets — et non de la génération des styles.

Tailwind inverse ce modèle. Il scanne le texte source à la recherche des noms de classes utilitaires et ne génère que ces règles. Aucun scan signifie aucun CSS. Le scan du code source fait partie de l’étape de compilation.

Pour un environnement qui lance un arbre d’application sans installation npm ni surveillance locale, les composants utilisant des classes utilitaires se heurtent à des obstacles que MUI n’a jamais rencontrés. button.tsx peut être transpilé correctement et produire néanmoins un contrôle non stylisé. Les tokens de classe apparaissent dans le DOM mais n’ont aucun sens, car aucune chaîne de traitement n’a généré de règles correspondantes.

C’est pourquoi les outils visuels butent à ce stade. Les variantes ne sont pas la cause racine ; c’est le stylage qui l’est.

Il existe une manière plus directe d’aborder le même problème. Le scanner de Tailwind lit les chaînes de code source. Les composants shadcn n’écrivent souvent jamais ces chaînes au point d’appel — ils appellent plutôt une fonction. Aucune valeur littérale "bg-destructive" ne peut apparaître sur cette ligne ; la composition en temps de exécution combine la carte CVA, les props reçus et les modifications apportées par l’appelant.

Une compilation normale gère bien cela : le fichier de la carte des variantes est scanné, les valeurs littérales complètes s’y trouvent et l’extracteur peut les détecter. La vraie règle de Tailwind est « ne pas construire des chaînes de classes comme text-${color}-500 ». Les cartes CVA contiennent des valeurs littérales complètes. shadcn représente le modèle recommandé, et non une solution de fortune ; il n’a pas besoin de liste de sécurité dans une compilation correcte.

Sans pipeline local, rien ne parcourt les fichiers à la recherche d’outils — donc la composition n’a nulle part où s’enregistrer. C’est pourquoi le mode sans installation est plus strict que le rappel général selon lequel Tailwind attend une compilation.

Solution de contournement : extraire les outils à partir du markup rendu

La solution viable consiste à abandonner le balayage des sources.

Le cadre de travail fait fonctionner l’environnement d’exécution authentique de Tailwind dans le navigateur — du code source, et non un analyseur personnalisé. Le choix du moteur dépend de la version principale déclarée : les applications v3 chargent une compilation Play CDN fournie dans Studio ; les versions v4+ chargent @tailwindcss/browser de la même manière. Le téléchargement depuis le réseau est réservé aux versions principales ne disposant d’aucune version intégrée — comme un projet hypothétique v5. Le fichier tailwind.config du projet est transmis au moteur afin que les extensions de thème restent fiables.

Pourquoi ne pas exécuter l’extracteur de Tailwind dans le navigateur et scanner tout de même la source ? Les mesures ont montré clairement le coût : une seule rendu déclenche déjà environ 40 à 340 lectures de fichiers hôtes, et sur un grand modèle une seule lecture peut prendre de 5 à 8 ms sans aucun avantage en termes de concurrence. Scanner le répertoire signifie lire tous les fichiers, et non seulement ceux qui ont été rendus. Malgré ce coût, les noms de classes assemblés en temps de exécution restent manquants. La collecte du DOM substitue « lire toute la structure » à « lire le résultat rendu une seule fois ».

Exécuter le moteur n’est que la moitié de l’histoire. C’est alors que la contrainte délicate apparaît.

L’engine est dédié au document et ne prend en compte que le DOM léger — il appelle observe(document.documentElement) sans jamais pénétrer dans des racines d’ombre. Le mode de conception affiche le template à l’intérieur d’une racine d’ombre afin d’assurer son isolation. L’engine se concentre donc sur le document externe, ne détecte aucun nom de classe de composant et ne génère rien. Engine correct, configuration correcte, feuille de style vide.

Le lien qui les reconnecte :

  1. Après l’affichage, parcourir le DOM d’ombre et collecter tous les tokens de classe présents sur les éléments réels.
  2. Copier ces tokens dans un nœud de destination caché du DOM léger que l’observateur peut voir.
  3. Laisser l’engine générer des règles spécifiquement pour les outils utilisés.
  4. Intégrer la feuille de style ainsi obtenue à nouveau dans la racine d’ombre.
  • Attachez un MutationObserver à la racine d’ombre — sous-arbre, attributeFilter: ['class'] — afin que les changements de route ultérieurs, le contenu différé et l’UI conditionnelle mettent toujours à jour le réceptacle.
  • Un détail crucial : le réceptacle doit être inséré à l’aide de la méthode native appendChild. Le canvas modifie document.body.appendChild pour rediriger les portails vers la racine d’ombre ; en utilisant cette version modifiée, le réceptacle est enfoui à l’intérieur de l’arbre d’ombre où le moteur ne peut pas le voir. Le pont échoue alors en silence.

    Celui-ci reste un véritable nœud light-DOM et continue d’accumuler des tokens au fur et à mesure de la navigation — les nouvelles routes fournissent les fonctionnalités nécessaires sans avoir à recharger le canvas.

    Un autre problème présente la même forme de défaillance, bien qu’il ne soit pas spécifique à shadcn. De nombreux templates Tailwind v3 Next définissent important: '#__next', ce qui limite l’application de tous les outils à cet identifiant racine. Le canvas ne contient pas #__next, ce qui empêche les outils de correspondre. Le sélecteur de configuration doit être redirigé vers la racine de rendu en temps réel, et le remplacement doit rester un sélecteur par identifiant, car la spécificité (1,0,0) est cruciale face au CSS propre au template.

    Les avantages de la collecte des données depuis le DOM :

    Tout nom de classe généré à l’exécution fonctionne. cn(), clsx, twMerge, CVA, expressions ternaires, tables de recherche ou concaténation à partir de props : le moteur n’a jamais besoin d’analyser ces formes. Au moment de la collecte, la composition a déjà eu lieu et le résultat se trouve dans un attribut class. Des templates comme shadcn-admin s’affichent correctement non pas en raison d’un cas particulier de shadcn, mais parce que cette approche est indifférente à la manière dont la chaîne a été construite.

    La limite honnête : seules les classes qui ont réellement été rendues reçoivent du CSS. Une génération normale de Tailwind analyse le code source et génère les deux branches de cond ? 'bg-red-500' : 'bg-blue-500', même lorsque seule l’une d’elles peut être affichée. Dans ce cas, la branche non utilisée reste sans style jusqu’à son rendu. Le MutationObserver la stylise lorsque l’état change — mais la première image de ce changement peut apparaître sans style. En pratique, cela se traduit par un bref espace vide lors du premier affichage d’une variante qui n’a jamais été vue.

    L’équilibre est à double tranchant. Prenons statusColors[res.status], où la clé provient d’une API. Aucune valeur littérale complète n’existe dans le code source, donc une compilation par balayage ne produit rien — des listes de sécurité existent pour pallier ce cas. La collecte du DOM ne se soucie pas de cela ; une fois que la classe est présente sur l’élément, celui-ci est collecté. Le balayage du code source couvre les branches qui n’ont pas été exécutées. La collecte du DOM couvre les noms de classes qui n’ont jamais été écrits sous forme de littéraux. Dans un navigateur sans processus de compilation, la deuxième option est largement imposée.

    Ce que cette base permet — et ce qu’elle ne permet pas

    Avec ce pont, un modèle de style shadcn-admin — noms de classes, variantes CVA, composition cn() — peut s’afficher sans installation, sans serveur de développement, avec le véritable moteur Tailwind et la configuration réelle du projet.

    L’avantage n’est pas que « l’édition visuelle est terminée ». Il s’agit plutôt d’éliminer une autre forme de contrainte.

    Ouvrir un modèle administratif inconnu signifie généralement cloner, installer, attendre, exécuter, cliquer pendant quelques minutes, puis quitter. La majeure partie de ce temps est consacrée à la machine. Le même problème se pose lorsqu’il s’agit de montrer à un collègue les trois états d’un composant, de vérifier si le mode sombre est prêt, ou d’analyser l’espacement d’une carte avant de proposer des modifications. Ces tâches ne prennent que quelques secondes une fois que l’élément est affiché à l’écran.

    C’est ce temps d’installation et d’attente qui disparaît. Un projet Shadcn peut utiliser ses vraies classes en quelques secondes, après quoi les fonctionnalités orientées code de Studio fonctionnent indépendamment du système de stylisation : sélection bidirectionnelle entre le code et la zone de travail, exploration d’un composant défini dans plusieurs fichiers, ainsi que la possibilité de savoir ce qui cesse de fonctionner si un fichier est supprimé. Ces fonctionnalités s’appuient sur la structure source, et non sur la manière dont le CSS a été généré. Elles nécessitent simplement d’abord une rendu réussi.

    Ce qui manque encore, c’est l’inspecteur de mise en forme : className est toujours modifié comme une propriété de chaîne brute, rien ne lit la carte CVA pour créer des contrôles de variantes, et les tokens de tailwind.config parviennent au moteur mais pas à la fenêtre d’édit. Les outils MUI disposent déjà d’une fonction d’édition de thème et de sx structuré ; ce n’est pas encore le cas pour Tailwind. Il vaut mieux préciser cette lacune plutôt que de laisser une démonstration donner une impression contraire.

    C’est l’ordre d’exécution qui est vraiment important, et il est facile de ne pas le remarquer de l’extérieur :

    Puisque Shadcn utilise des outils fonctionnels plutôt que des propriétés de style, il faut insérer les chaînes de classes générées en temps réel sur la page avec du CSS véritable avant que tout inspecteur structuré ne puisse les modifier. Le faire directement dans le navigateur est compliqué : le moteur officiel surveille le DOM léger, tandis que le mode de conception travaille à l’intérieur d’une racine shadow. Compléter cela en extrayant les classes du DOM en temps réel — et non des fichiers — comble cette lacune. Cette base est enfin disponible.

    La couche qui vient par-dessus est plus petite que cette base, mais pas simple à mettre en œuvre. Une carte CVA correspond à un objet littéral dans un fichier local ; la lecture de ce dernier et la création d’un sélecteur de variant constituent des tâches UI ordinaires. En revanche, réécrire les modifications n’est pas si évident. Un nom de classe peut provenir en même temps de plusieurs sources de composition :

    className={cn("border-b", isActive && "bg-red-500", buttonVariants({ variant }))}
    

    Si un panneau définit une marge de 6, il faut choisir quel élément littéral modifier : la chaîne de base, l’élément conditionnel ou la définition de variante partagée utilisée par tous les autres boutons. Parfois, il n’existe pas de réponse unique et correcte, seulement des principes directeurs. Ce problème de réécriture est la prochaine étape difficile, et il l’est d’une manière différente de celle du pont de stylisation.

    Au début des travaux, il aurait été facile de deviner quel aspect était le plus difficile — et de se tromper.

    Les équipes qui travaillent avec shadcn et se soucient du comportement attendu de cette interface de contrôle — des listes déroulantes de variantes gérées par CVA, des contrôles d’espacement qui réécrivent les utilitaires plutôt que des styles en ligne, ainsi que des règles déterminant quel élément littéral doit faire l’objet d’une réécriture — sont les interlocuteurs idéaux pour façonner ce niveau avant sa mise en production plutôt qu’après.