La mayoría de las herramientas de diseño tratan el código como un problema posterior. Diseñas en un archivo propietario, lo entregas y esperas que el ingeniero lea la especificación correctamente. Penpot está construido sobre la suposición opuesta. El formato de archivo es abierto, el modelo de tokens sigue un estándar del W3C de forma que se mapea a variables de código sin una capa de traducción, y la API de plugins permite leer el diseño programáticamente.
Pasé una sesión manejando Penpot a través de su API de plugins para consolidar un sistema de diseño y reconstruir un tablero guía. El resto de este post es lo que aprendí sobre cómo Penpot está construido para este flujo de trabajo, y la fricción que encontré en el camino.
El formato de archivo es abierto
Design-to-code empieza por el archivo. Si tu diseño vive en un blob binario cerrado, estás atascado con el export que el proveedor te dé.
Un archivo .penpot es un ZIP que contiene metadatos JSON legibles junto a assets binarios como imágenes. No es un blob propietario. El formato v3 contiene un manifiesto, metadatos del archivo, páginas, shapes, assets de biblioteca, objetos de almacenamiento, datos de plugins e imágenes embebidas.
La historia vale la pena conocerla. El formato v1 de .penpot era un blob binario propio. Penpot también ofrecía un export .zip separado que era SVG y JSON, abierto pero ineficiente. El formato v3 actual de .penpot es lo mejor de ambos: un contenedor ZIP con metadatos JSON que es tanto inspeccionable como eficiente. Tus datos de diseño son tuyos, en formatos que puedes leer.
Esa es la base sobre la que todo lo demás se construye. Puedes sacar tus datos sin pedir permiso.
Los tokens son el contrato
Los tokens son el puente entre diseño y código. Un token en Penpot es un valor con nombre: un color, un estilo de tipografía, un radio de borde, una cantidad de espaciado. Cuando aplicas un token a un shape, el shape recuerda tanto el valor como el enlace.
Cuando lees un shape a través de la API de plugins, obtienes ambos:
shape.tokens.fill // "canvas"
shape.fills // [{ fillColor: "#000000", fillOpacity: 1 }]shape.tokens.fill es el nombre del token, como string. shape.fills es el valor visual resuelto. El enlace y el valor, lado a lado.
Ese es el contrato. Si el token de Penpot primary se mapea a la variable CSS --primary, entonces un shape con tokens.fill === "primary" se mapea directamente a background: var(--primary). El nombre del token en el archivo de diseño se convierte en el nombre del token en la base de código. Sin capa de traducción, sin adivinar qué valor hex mapea a qué rol.
Esto funciona porque los tokens de Penpot siguen el W3C Design Tokens Format Module. Penpot es la primera herramienta de diseño en integrar nativamente este estándar, construido en colaboración con Tokens Studio. Los nombres de los tokens no son específicos de Penpot. Están en un formato que cualquier herramienta que soporte el estándar puede leer.
Los tokens viven en conjuntos
La forma de la API refleja el modelo. TokenCatalog no te permite añadir tokens directamente. Solo expone operaciones de conjuntos:
interface TokenCatalog {
addSet(...)
getSetById(...)
sets
themes
}
interface TokenSet {
tokens: Token[]
addToken({ type, name, value }): Token
}Los tokens se crean en un TokenSet, no en el catálogo. La documentación lo dice claramente: "Tokens are contained in sets." No hay tokens flotando libremente.
Un detalle a conocer: Penpot lee los puntos en los nombres de tokens como rutas. Puse prefijos de carpeta dentro de los nombres de tokens, como colors.primary, y obtuve una carpeta extra colors bajo el tipo de token Colors. Nombres planos lo arreglaron.
Los temas son cómo funcionan Light y Dark
Viniendo de Figma, esperarías que Light y Dark sean modos de una colección. Penpot lo divide diferente. Light y Dark son temas, y un tema es una combinación de conjuntos.
Pones tus tokens base en un conjunto: colores de marca, espaciado, radios. Pones colores específicos de Light en un conjunto Light y colores específicos de Dark en un conjunto Dark. Luego los combinas en temas. El tema Light habilita los conjuntos Base y Light. El tema Dark habilita los conjuntos Base y Dark. Cambias el tema y todo el tablero se intercambia.
La API refleja esto. TokenCatalog expone tanto sets como themes como hermanos. Construyes conjuntos, luego configuras qué conjuntos activa cada tema. La propiedad themes estaba ahí en el snippet de arriba, y pasé por encima la primera vez.
Cómo se compara con Figma
Si vienes de Figma, el mapeo es: las colecciones de Figma corresponden a los conjuntos de Penpot, y los modos de Figma corresponden a los temas de Penpot. Una colección de Figma contiene variables relacionadas, y un modo almacena valores paralelos para diferentes contextos. En Penpot, un conjunto contiene tokens relacionados, y un tema combina conjuntos para un contexto.
La diferencia es el formato. Las variables de Figma usan un formato propietario. Sacar tokens de Figma en el formato estándar del W3C requiere un plugin como Tokens Studio. Penpot exporta el formato W3C nativamente. Ambos implementan el mismo concepto. Uno habla el estándar, uno habla su propio dialecto.
Las colisiones de nombres son por conjunto, no por tipo
Quería un conjunto Base con todo: colores, tipografía, radios, espaciado. La nomenclatura natural para radios y espaciado era xs, sm, md, lg, xl.
Penpot lo rechazó:
A token already exists at the path: xs or at a prefix thereof.La regla de unicidad es por conjunto, a través de todos los tipos, no por tipo. xs no puede ser a la vez un token borderRadius y un token spacing dentro del mismo conjunto.
La solución fueron los prefijos: radius-xs, radius-sm, space-xs, space-sm. Colores y tipografía mantuvieron nombres planos porque no comparten nombres con nada más. La colisión solo apareció donde dos escalas querían los mismos nombres cortos. Vale la pena saberlo antes de diseñar tu esquema de nombres de tokens.
Dónde aplican los tokens (y dónde no)
No todos los tokens aplican a todas las propiedades. La API lo hace cumplir.
Quería mostrar cada token de espaciado como una pequeña barra cuya longitud coincidiera con el valor del token, y aplicar el token a esa barra. La API lo rechazó:
Field message is invalid.Probé marginLeft, paddingLeft, paddingTop. Todos rechazados con el mismo error. Los objetivos documentados para tokens de espaciado son las propiedades de diseño flex: rowGap, columnGap, las props de padding y las props de margin. En la práctica, en un tablero con un layout flex añadido a través de addFlexLayout(), solo rowGap y columnGap aceptaron el token. Las propiedades de padding y margin siguieron lanzando el error de validación.
Los tokens de espaciado no son dimensiones de propósito general. Aplican a los gaps de layout. Para representar un token de espaciado visualmente, construí un pequeño tablero con un layout flex y configuré su columnGap al token. Los dos rectángulos dentro del tablero, separados por ese gap, son la representación visible del valor de espaciado.
Los tokens de color, tipografía y radio de borde aplican limpiamente a fills, texto y esquinas. Esto coincide con cómo los usarías en código: un color va en un fill, un estilo de tipografía va en texto, un radio va en una esquina. El espaciado va en los gaps entre elementos, que en Penpot significa gaps de layout flex.
Los componentes se mapean por especificación, no por magia
Un componente de Penpot es un objeto de diseño, no un archivo de código. Cuando manipulé componentes a través de la API, estaba cambiando objetos dentro del archivo de diseño de Penpot: rectángulos, texto, grupos, componentes de biblioteca. No React, no CSS, no archivos en el repo.
Un componente de Penpot es un objeto de diseño reutilizable con una instancia principal y copias. Editas el componente principal y las instancias se actualizan. Tiene propiedades visuales: fills, strokes, tipografía, layout, tokens. No vive en el repositorio.
Para convertir un componente de Penpot en código, lo mapeas a mano. Un componente de diseño button-primary se convierte en un botón de React con CSS que usa los mismos nombres de tokens. Una pantalla de Penpot se convierte en una vista de la app. Los nombres de tokens son el puente. El objeto de diseño y el componente de código están relacionados por nombrado y especificación, no por ninguna conversión automática.
Esta es la versión honesta de design-to-code. Las herramientas te dan un vocabulario compartido (tokens) y un formato de archivo abierto. No te escriben los componentes. El mapeo sigue siendo una persona mirando el diseño y escribiendo el código, pero el contrato es explícito en lugar de adivinado. Un formato como DESIGN.md puede llevar esos nombres de tokens al repo, de forma que un agente de IA o un desarrollador leen la misma fuente que la herramienta de diseño exporta.
La API te permite inspeccionar todo
El tooling de design-to-code necesita leer el diseño programáticamente. La API de plugins está construida para eso.
History tiene dos capas, lo que da una idea de cómo está organizada la API. El undo history es la línea de tiempo de edición dentro de la sesión. La API de plugins puede agrupar ediciones en un paso de undo con penpot.history.undoBlockBegin() y penpot.history.undoBlockFinish(block). Envuelves un lote de ediciones en un bloque y un undo revierte todo.
Las versiones de archivo son checkpoints guardados. La API expone penpot.currentFile.saveVersion("label") y penpot.currentFile.findVersions(). Una versión guardada es un punto al que puedes volver más tarde, entre sesiones. Guardé el estado consolidado como Version 1 a través de saveVersion. El valor de retorno tuvo un bug de serialización en el wrapper, pero findVersions() confirmó que la versión existía con la etiqueta correcta. El guardado funcionó aunque el valor de retorno no se serializó limpiamente.
El punto es que el archivo de diseño es consultable. Puedes recorrer el árbol de shapes, leer tokens y fills, listar conjuntos, guardar versiones y rastrear qué cambió. Eso es de lo que una pipeline de design-to-code necesita alimentarse.
Export y la realidad práctica
El export es donde encontré más fricción, y los logs valen la pena leerlos porque muestran cómo funciona el exporter.
La herramienta de export seguía fallando. Primero con http error, luego con timeouts después de 30 segundos. Un tablero de prueba de 100x100 con un fill rojo también hizo timeout, así que el contenido del tablero no era el problema.
Los logs del exporter de Penpot mostraron por qué:
ERR [app.handlers.export-shapes] hint="unexpected error on single export"
page.goto: net::ERR_CONNECTION_REFUSED at http://penpot-frontend/render.htmlEl exporter es un navegador headless que navega a una URL de render y le toma una captura de pantalla. Estaba intentando alcanzar http://penpot-frontend/render.html y recibiendo conexión rechazada. Eso es un problema de despliegue. El contenedor del exporter no podía resolver o alcanzar el hostname del servicio frontend.
Después del fix, los logs cambiaron:
INF [app.renderer.bitmap] uri="https://penpot.example.com/render.html?..."El exporter ahora alcanzaba el frontend por la URL pública y renderizaba. La línea exec:handle:end mostró que el navegador terminó su trabajo. La herramienta MCP a través de la que llamaba seguía haciendo timeout a los 30 segundos. El render se completó en el servidor. El wrapper que me devolvía el resultado era el cuello de botella.
La conclusión: cuando un export de Penpot falla, lee los logs del exporter. El error nombra la URL que el exporter intentaba alcanzar. ERR_CONNECTION_REFUSED a un hostname interno significa que el exporter no puede encontrar el frontend. Un timeout sin error en los logs normalmente significa que el render en sí es lento, o que el wrapper de la herramienta es el límite, no Penpot.
Lo que Penpot hace bien para Design to Code
Penpot no es magia. Sigues escribiendo el código. Lo que Penpot te da es una herramienta de diseño construida sobre suposiciones que hacen design-to-code posible:
- Un formato de archivo abierto que puedes inspeccionar y extraer.
- Un modelo de tokens basado en el estándar W3C, donde el enlace y el valor son ambos legibles, y los temas manejan Light y Dark sin duplicar archivos.
- Una API de plugins que permite al tooling recorrer el diseño y leer los contratos.
- Componentes que se mapean por nombrado y especificación, sin lock-in sobre cómo los implementas.
Leer más
Design Tokens, DESIGN.md y Design to Code
Qué hacen realmente los design tokens (y qué no), cómo DESIGN.md se convirtió en el formato que leen los agentes de IA, y cómo el movimiento design-to-code está cambiando la entrega de diseño a código de producción.
Una base de código, Web y Escritorio
Cómo diseñar una app de Tauri para que el mismo código React corra en el navegador y en el escritorio, poniendo las diferencias de runtime detrás de una interfaz de servicio.
Cómo funcionan las apps móviles
Las tres formas de construir una app móvil, qué ejecuta realmente cada una por debajo, y el patrón de arquitectura compartido detrás de React Native, Flutter y Tauri mobile.
