La plupart des outils de design traitent le code comme un souci en aval. Tu conçois dans un fichier propriétaire, tu le transmets, et tu espères que l'ingénieur lira bien la spec. Penpot est construit sur l'hypothèse inverse. Le format de fichier est ouvert, le modèle de tokens suit un standard du W3C de sorte qu'il se mappe aux variables de code sans couche de traduction, et l'API de plugins permet de lire le design programmatiquement.
J'ai passé une session à piloter Penpot via son API de plugins pour consolider un design system et reconstruire un panneau guide. Le reste de ce post est ce que j'ai appris sur la façon dont Penpot est construit pour ce workflow, et les frictions que j'ai rencontrées en chemin.
Le format de fichier est ouvert
Le design-to-code commence par le fichier. Si ton design vit dans un blob binaire fermé, tu es coincé avec l'export que le vendeur te donne.
Un fichier .penpot est une archive ZIP contenant des métadonnées JSON lisibles à côté d'assets binaires comme des images. Pas un blob propriétaire. Le format v3 contient un manifeste, des métadonnées de fichier, des pages, des shapes, des assets de bibliothèque, des objets de stockage, des données de plugins et des images embarquées.
L'histoire vaut la peine d'être connue. Le format v1 de .penpot était un blob binaire propriétaire. Penpot offrait aussi un export .zip séparé qui était du SVG et du JSON, ouvert mais inefficace. Le format v3 actuel de .penpot est le meilleur des deux : un conteneur ZIP avec des métadonnées JSON qui est à la fois inspectable et efficace. Tes données de design t'appartiennent, dans des formats que tu peux lire.
C'est le fondement sur lequel tout le reste se construit. Tu peux extraire tes données sans permission.
Les tokens sont le contrat
Les tokens sont le pont entre design et code. Un token dans Penpot est une valeur nommée : une couleur, un style typographique, un rayon de bordure, une valeur d'espacement. Quand tu appliques un token à un shape, le shape retient à la fois la valeur et la liaison.
Quand tu lis un shape via l'API de plugins, tu obtiens les deux :
shape.tokens.fill // "canvas"
shape.fills // [{ fillColor: "#000000", fillOpacity: 1 }]shape.tokens.fill est le nom du token, en tant que string. shape.fills est la valeur visuelle résolue. La liaison et la valeur, côte à côte.
C'est le contrat. Si le token Penpot primary se mappe à la variable CSS --primary, alors un shape avec tokens.fill === "primary" se mappe directement à background: var(--primary). Le nom du token dans le fichier de design devient le nom du token dans la codebase. Pas de couche de traduction, pas de devinette sur quelle valeur hex correspond à quel rôle.
Ça fonctionne parce que les tokens de Penpot suivent le W3C Design Tokens Format Module. Penpot est le premier outil de design à intégrer nativement ce standard, construit en collaboration avec Tokens Studio. Les noms des tokens ne sont pas spécifiques à Penpot. Ils sont dans un format que tout outil supportant le standard peut lire.
Les tokens vivent dans des sets
La forme de l'API reflète le modèle. TokenCatalog ne te laisse pas ajouter des tokens directement. Il n'expose que des opérations de set :
interface TokenCatalog {
addSet(...)
getSetById(...)
sets
themes
}
interface TokenSet {
tokens: Token[]
addToken({ type, name, value }): Token
}Les tokens sont créés sur un TokenSet, pas sur le catalogue. La doc le dit clairement : "Tokens are contained in sets." Il n'y a pas de token flottant librement.
Un détail à connaître : Penpot lit les points dans les noms de tokens comme des chemins. J'ai mis des préfixes de dossier dans les noms de tokens, comme colors.primary, et j'ai obtenu un dossier colors supplémentaire sous le type de token Colors. Des noms plats l'ont corrigé.
Les thèmes sont comment fonctionnent Light et Dark
En venant de Figma, tu t'attendrais à ce que Light et Dark soient des modes d'une collection. Penpot divise ça autrement. Light et Dark sont des thèmes, et un thème est une combinaison de sets.
Tu mets tes tokens de base dans un set : couleurs de marque, espacement, rayons. Tu mets les couleurs spécifiques à Light dans un set Light et les couleurs spécifiques à Dark dans un set Dark. Ensuite tu les combines en thèmes. Le thème Light active les sets Base et Light. Le thème Dark active les sets Base et Dark. Tu changes le thème et tout le panneau bascule.
L'API reflète ça. TokenCatalog expose sets et themes comme frères. Tu construis des sets, puis tu configures quels sets chaque thème active. La propriété themes était là dans le snippet ci-dessus, et je suis passé à côté la première fois.
Comment ça se compare à Figma
Si tu viens de Figma, le mapping est : les collections Figma correspondent aux sets Penpot, et les modes Figma correspondent aux thèmes Penpot. Une collection Figma contient des variables liées, et un mode stocke des valeurs parallèles pour différents contextes. Dans Penpot, un set contient des tokens liés, et un thème combine des sets pour un contexte.
La différence, c'est le format. Les variables de Figma utilisent un format propriétaire. Sortir des tokens de Figma au format standard W3C nécessite un plugin comme Tokens Studio. Penpot exporte le format W3C nativement. Les deux implémentent le même concept. L'un parle le standard, l'un parle son propre dialecte.
Les collisions de noms sont par set, pas par type
Je voulais un set Base avec tout : couleurs, typographie, rayons, espacement. Le nommage naturel pour les rayons et l'espacement était xs, sm, md, lg, xl.
Penpot a refusé :
A token already exists at the path: xs or at a prefix thereof.La règle d'unicité est par set, à travers tous les types, pas par type. xs ne peut pas être à la fois un token borderRadius et un token spacing dans le même set.
La solution, c'était des préfixes : radius-xs, radius-sm, space-xs, space-sm. Couleurs et typographie ont gardé des noms plats parce qu'ils ne partagent pas de noms avec autre chose. La collision n'est apparue que là où deux échelles voulaient les mêmes noms courts. Bon à savoir avant de concevoir ton schéma de nommage de tokens.
Où les tokens s'appliquent (et où ils ne s'appliquent pas)
Tous les tokens ne s'appliquent pas à toutes les propriétés. L'API le fait respecter.
Je voulais montrer chaque token d'espacement comme une petite barre dont la longueur correspondait à la valeur du token, et appliquer le token à cette barre. L'API a refusé :
Field message is invalid.J'ai essayé marginLeft, paddingLeft, paddingTop. Tous refusés avec la même erreur. Les cibles documentées pour les tokens d'espacement sont les propriétés de layout flex : rowGap, columnGap, les props de padding et les props de margin. En pratique, sur un panneau avec un layout flex ajouté via addFlexLayout(), seuls rowGap et columnGap acceptaient le token. Les propriétés de padding et de margin continuaient à lever l'erreur de validation.
Les tokens d'espacement ne sont donc pas des dimensions à usage général. Ils s'appliquent aux gaps de layout. Pour représenter un token d'espacement visuellement, j'ai construit un petit panneau avec un layout flex et réglé son columnGap sur le token. Les deux rectangles à l'intérieur du panneau, séparés par ce gap, sont la représentation visible de la valeur d'espacement.
Les tokens de couleur, de typographie et de rayon de bordure s'appliquent proprement aux fills, au texte et aux coins. Ça correspond à la façon dont tu les utiliserais en code : une couleur va sur un fill, un style typographique va sur du texte, un rayon va sur un coin. L'espacement va sur les gaps entre les éléments, ce qui dans Penpot signifie les gaps de layout flex.
Les composants se mappent par spec, pas par magie
Un composant Penpot est un objet de design, pas un fichier de code. Quand je manipulais des composants via l'API, je changeais des objets à l'intérieur du fichier de design Penpot : des rectangles, du texte, des groupes, des composants de bibliothèque. Pas React, pas CSS, pas des fichiers dans le repo.
Un composant Penpot est un objet de design réutilisable avec une instance principale et des copies. Tu modifies le composant principal et les instances se mettent à jour. Il a des propriétés visuelles : fills, strokes, typographie, layout, tokens. Il ne vit pas dans le repository.
Pour transformer un composant Penpot en code, tu le mappes à la main. Un composant de design button-primary devient un bouton React avec du CSS qui utilise les mêmes noms de tokens. Un écran Penpot devient une vue de l'app. Les noms de tokens sont le pont. L'objet de design et le composant de code sont liés par nommage et spec, pas par une conversion automatique.
C'est la version honnête du design-to-code. Les outils te donnent un vocabulaire partagé (les tokens) et un format de fichier ouvert. Ils ne t'écrivent pas tes composants. Le mapping est encore une personne qui regarde le design et écrit le code, mais le contrat est explicite au lieu d'être deviné. Un format comme DESIGN.md peut porter ces noms de tokens dans le repo, de sorte qu'un agent IA ou un développeur lit la même source que l'outil de design exporte.
L'API te laisse tout inspecter
Le tooling de design-to-code a besoin de lire le design programmatiquement. L'API de plugins est construite pour ça.
History a deux couches, ce qui donne une idée de la façon dont l'API est organisée. L'undo history est la timeline d'édition dans la session. L'API de plugins peut grouper des éditions en une seule étape d'undo avec penpot.history.undoBlockBegin() et penpot.history.undoBlockFinish(block). Tu enveloppes un lot d'éditions dans un bloc et un seul undo annule tout.
Les versions de fichier sont des checkpoints sauvegardés. L'API expose penpot.currentFile.saveVersion("label") et penpot.currentFile.findVersions(). Une version sauvegardée est un point où tu peux revenir plus tard, entre sessions. J'ai sauvegardé l'état consolidé comme Version 1 via saveVersion. La valeur de retour a touché un bug de sérialisation dans le wrapper, mais findVersions() a confirmé que la version existait avec le bon label. La sauvegarde a fonctionné même si la valeur de retour ne s'est pas sérialisée proprement.
Le point, c'est que le fichier de design est interrogeable. Tu peux parcourir l'arbre des shapes, lire les tokens et les fills, lister les sets, sauvegarder des versions et tracer ce qui a changé. C'est de quoi une pipeline de design-to-code a besoin pour s'alimenter.
L'export et la réalité pratique
L'export, c'est là que j'ai eu le plus de friction, et les logs valent la peine d'être lus parce qu'ils montrent comment l'exporter fonctionne.
L'outil d'export continuait à échouer. D'abord avec http error, puis avec des timeouts après 30 secondes. Un panneau de test de 100x100 avec un fill rouge a aussi fait un timeout, donc le contenu du panneau n'était pas le problème.
Les logs de l'exporter Penpot ont montré pourquoi :
ERR [app.handlers.export-shapes] hint="unexpected error on single export"
page.goto: net::ERR_CONNECTION_REFUSED at http://penpot-frontend/render.htmlL'exporter est un navigateur headless qui navigue vers une URL de rendu et en prend une capture. Il essayait d'atteindre http://penpot-frontend/render.html et recevait une connexion refusée. C'est un problème de déploiement. Le conteneur de l'exporter ne pouvait pas résoudre ou atteindre le hostname du service frontend.
Après le fix, les logs ont changé :
INF [app.renderer.bitmap] uri="https://penpot.example.com/render.html?..."L'exporter atteignait maintenant le frontend via l'URL publique et rendait. La ligne exec:handle:end a montré que le navigateur avait terminé son travail. L'outil MCP à travers lequel j'appelais continuait à faire un timeout à 30 secondes. Le rendu s'est terminé côté serveur. Le wrapper qui me renvoyait le résultat était le goulot d'étranglement.
La conclusion : quand un export Penpot échoue, lis les logs de l'exporter. L'erreur nomme l'URL que l'exporter essayait d'atteindre. ERR_CONNECTION_REFUSED sur un hostname interne signifie que l'exporter ne peut pas trouver le frontend. Un timeout sans erreur dans les logs signifie généralement que le rendu lui-même est lent, ou que le wrapper de l'outil est la limite, pas Penpot.
Ce que Penpot fait bien pour le Design to Code
Penpot n'est pas magique. Tu écris toujours le code. Ce que Penpot te donne, c'est un outil de design construit sur des hypothèses qui rendent le design-to-code possible :
- Un format de fichier ouvert que tu peux inspecter et extraire.
- Un modèle de tokens basé sur le standard W3C, où la liaison et la valeur sont toutes deux lisibles, et les thèmes gèrent Light et Dark sans dupliquer les fichiers.
- Une API de plugins qui permet au tooling de parcourir le design et de lire les contrats.
- Des composants qui se mappent par nommage et spec, sans lock-in sur la façon dont tu les implémentes.
Lire la suite
Design Tokens, DESIGN.md et Design to Code
Ce que les design tokens font vraiment (et ce qu'ils ne font pas), comment DESIGN.md est devenu le format que les agents IA lisent, et comment le mouvement design-to-code change le handoff du design vers le code de production.
Une base de code, Web et Bureau
Comment concevoir une app Tauri pour que le même code React tourne dans le navigateur et sur le bureau, en mettant les différences de runtime derrière une interface de service.
Comment fonctionnent les applications mobiles
Les trois façons de construire une app mobile, ce que chacune fait réellement sous le capot, et le schéma d'architecture partagé derrière React Native, Flutter et Tauri mobile.
