Inicio / Artículos

Por qué los editores visuales tienen dificultades con los componentes de shadcn UI

Los editores visuales tienen dificultades con shadcn ui porque los mapas CVA se encuentran en tu repositorio y Tailwind necesita una compilación; la recopilación de clases DOM conecta el modo de diseño en React.

Editor: REACTAPP.TOP9 min de lectura2063 palabras

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

shadcn/ui se presenta como una solución de propiedad mediante copiar y pegar: ejecuta la CLI, coloca components/ui/button.tsx en el repositorio y trata el archivo como código propio del proyecto. No existe un ciclo de actualizaciones continuas, ni disputas por wrappers, ni competencias con estilosheets de paquetes externos.

Esa promesa plantea una pregunta práctica: si el componente ya está en el proyecto, ¿por qué cambiarlo sigue significando abrir el archivo manualmente?

La respuesta es técnica y no es la primera suposición que hacen la mayoría de los equipos.

Los constructores visuales esperan un catálogo de propiedades. Los mapas CVA de propiedad propia son diferentes.

Los constructores visuales de React suelen asumir que los componentes provienen de un paquete con una interfaz de propiedades estable y documentada. Material UI es el ejemplo típico: un botón expone valores enumerados como variant, size y color. Una herramienta puede leer estos tipos, mostrar menús desplegables y escribir cadenas de texto nuevamente. El componente sigue siendo una caja negra con un panel de control etiquetado en su parte frontal.

En cambio, abra un botón 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" }, } )

No existe un catálogo externo de propiedades. Solo hay un mapa de variantes: código TypeScript simple en un archivo propiedad del proyecto.

La opinión general es que esto resulta peor para las herramientas de desarrollo: no hay nada que inspeccionar en node_modules, no existe un sitio de documentación extraído, y cada fork evoluciona de forma independiente. Los editores diseñados como paquetes realmente no pueden adaptarse a ese formato.

La interpretación más sólida apunta en la dirección opuesta. Un mapa CVA contiene más información que una tabla de propiedades, y esa información ya está almacenada en el disco.

Una tabla de propiedades indica que variant acepta "contained". No especifica qué representa "contained". Esa correspondencia se compila dentro de la biblioteca. Para conocerla hay que leer la documentación, adivinar o probarla en tiempo real.

El mapa CVA indica que variant: "destructive" es bg-destructive text-destructive-foreground. La relación es un dato en el código fuente bajo control local. Nada está oculto, porque nada se guardó de forma separada.

Por lo tanto, la afirmación es específica:

La edición visual estructurada necesita saber qué función cumple un control, no solo que existe. Los componentes empaquetados ocultan ese conocimiento en sus partes internas compiladas. shadcn lo mantiene como un objeto literal en el repositorio. Según este criterio, shadcn debería ser la biblioteca más fácil de editar visualmente, y no la más difícil.

Lo que aún falta en el mercado es una edición estructurada de variantes impulsada por tu propio mapa CVA en tu propio árbol. Existen visualizadores de Tailwind y editores de temas de shadcn, pero las herramientas de temas reescriben las variables CSS. Eso cambia a qué se resuelve bg-primary; no cambia qué variante utiliza un botón ni qué utilidades contiene esa variante. El obstáculo para la segunda tarea se encuentra un nivel por debajo de las variantes.

El verdadero obstáculo: las clases de utilidad solo se convierten en CSS después de un análisis

MUI aplica el estilo en tiempo de ejecución. Un objeto sx={{ p: 2 }} se convierte en CSS mientras el componente se renderiza; no hay nada que calcular por adelantado ni nada que escanear. Ejecutar MUI en un navegador sin instalación local sigue siendo difícil, pero la dificultad radica en la resolución de módulos, las instancias de temas y emociones singulares, y las versiones coherentes del paquete, no en la generación de estilo.

Tailwind invierte este modelo. Escanea el texto fuente en busca de nombres de clases utilitarias y emite únicamente esas reglas. Al no haber escaneo, no hay CSS. El escaneo del código fuente es un paso de compilación.

En el caso de un entorno que inicia un árbol de aplicaciones sin necesidad de una instalación con npm ni de un monitor local, los componentes basados en clases utilitarias se enfrentan a obstáculos que MUI nunca tuvo. button.tsx puede transpilarse correctamente y, aun así, generar un control sin estilo. Los tokens de clase aparecen en el DOM pero no significan nada, ya que ninguna pipeline generó reglas correspondientes.

Esa es la razón por la cual las herramientas visuales se detienen en el umbral. Las variantes no son la causa raíz; lo es el estilo.

Existe una forma más directa de abordar el mismo problema. El analizador de Tailwind lee las cadenas de código fuente. Los componentes de shadcn a menudo nunca escriben esas cadenas en el lugar donde se las llama, sino que invocan una función en su lugar. No puede aparecer literalmente "bg-destructive" en esa línea; la composición en tiempo de ejecución combina el mapa CVA, los parámetros recibidos y las sobrescripciones del llamante.

Una compilación normal maneja esto bien: se escanea el archivo del mapa de variantes, allí se encuentran los literales completos y el extractor los detecta. La regla real de Tailwind es “no construir cadenas de clases como text-${color}-500”. Los mapas CVA almacenan literales completos. shadcn es el patrón recomendado, no una solución temporal, y no necesita lista de seguridad en una compilación adecuada.

Sin un pipeline local, nada recorre los archivos en busca de utilidades; por lo tanto, la composición no tiene dónde registrarse. Esa es la razón por la cual el entorno sin instalación es más estricto que el recordatorio general de que Tailwind espera una compilación.

Solución alternativa: extraer utilidades del marcado renderizado

La solución viable es abandonar el escaneo de fuentes.

El entorno propio impulsa el tiempo de ejecución auténtico de Tailwind en el navegador: código original, no un analizador hecho a mano. La elección del motor sigue la versión declarada: las aplicaciones v3 cargan una compilación de Play CDN incluida en Studio; las v4+ cargan @tailwindcss/browser de la misma manera. La descarga desde la red está reservada para versiones sin versión empaquetada correspondiente, como en un proyecto hipotético v5. El archivo tailwind.config del proyecto se pasa al motor para que las extensiones de tema sigan siendo autoritativas.

¿Por qué no ejecutar el extractor de Tailwind en el navegador y escanear la fuente de todos modos? Las mediciones dejaron claro el costo: una sola renderización ya provoca entre 40 y 340 lecturas de archivos del servidor, y en una plantilla grande una sola lectura puede tardar de 5 a 8 ms sin obtener ventaja alguna por la concurrencia. Escanear el repositorio implica leer cada archivo, no solo aquellos que se han renderizado. Incluso después de ese costo, siguen perdiéndose los nombres de clases generados en tiempo de ejecución. La recolección del DOM intercambia “leer todo el árbol” por “leer el resultado renderizado una sola vez”.

Ejecutar el motor es solo la mitad de la historia. Luego aparece esa complicada restricción.

El motor está limitado al ámbito del documento y solo observa el DOM convencional; llama a observe(document.documentElement) y nunca accede a las raíces de sombra. El modo de diseño renderiza la plantilla dentro de una raíz de sombra para su aislamiento. Por lo tanto, el motor solo examina el documento exterior, no ve los nombres de las clases de los componentes y no emite nada. Motor correcto, configuración correcta, hoja de estilos vacía.

El vínculo que los vuelve a conectar:

  1. Después del renderizado, recorre el DOM de sombra y recopila todos los tokens de clase presentes en los elementos reales.
  2. Copia esos tokens a un nodo oculto del DOM convencional al que el observador puede acceder.
  3. Deja que el motor genere reglas específicamente para las utilidades en uso.
  4. Introduce la hoja de estilos resultante de nuevo en la raíz de sombra.
  • Adjunte un MutationObserver en la raíz de sombra—subárbol, attributeFilter: ['class']—para que los cambios de ruta posteriores, el contenido perezoso y la interfaz condicional sigan actualizando el sink.
  • Un detalle crucial es que el sink debe insertarse con el método nativo appendChild. El canvas modifica document.body.appendChild para redirigir los portales hacia la raíz de sombra; al usar esa ruta modificada, el sink queda oculto dentro del árbol de sombra donde el motor no puede verlo. Por lo tanto, el puente deja de funcionar en silencio.

    Ese sink sigue siendo un nodo real del light-DOM y continúa acumulando tokens a medida que ocurre la navegación: las rutas nuevas aportan las utilidades necesarias sin tener que recargar el canvas.

    Otro problema común presenta la misma forma de fallo, aunque no es específico de shadcn. Muchas plantillas de Tailwind v3 Next establecen important: '#__next', lo que limita el alcance de todas las utilidades a ese ID raíz. El canvas no cuenta con #__next, por lo que las utilidades dejan de coincidir. El selector de configuración debe redirigirse al punto de renderizado en tiempo real, y el reemplazo debe seguir siendo un selector de ID, ya que la especificidad (1,0,0) es determinante frente al CSS propio de la plantilla.

    La ventaja de obtener datos del DOM es:

    Cualquier nombre de clase generado en tiempo de ejecución funciona. cn(), clsx, twMerge, CVA, expresiones ternarias, tablas de búsqueda o concatenación a partir de propiedades: el motor nunca necesita analizar esas formas. Para el momento de la recopilación, la composición ya se ha realizado y el resultado se encuentra en un atributo class. Plantillas como shadcn-admin se renderizan correctamente no por ser un caso especial de shadcn, sino porque el enfoque es indiferente a la forma en que se construyó la cadena.

    El límite real: solo las clases que se han renderizado realmente reciben estilos CSS. Una compilación normal de Tailwind escanea el código fuente y emite ambas ramas de cond ? 'bg-red-500' : 'bg-blue-500' incluso cuando solo una puede mostrarse. En este caso, la rama no utilizada permanece sin estilo hasta que se renderiza. MutationObserver la estila cuando cambia el estado, pero el primer fotograma de ese cambio puede mostrarla sin estilo temporalmente. En la práctica, esto se manifiesta como un breve espacio en blanco al activar por primera vez una variante que nunca se había visto.

    El compromiso afecta a ambos lados. Considere statusColors[res.status], donde la clave proviene de una API. No puede existir un literal de utilidad completo en el código fuente, por lo que una compilación por escaneo no genera nada; existen listas de seguridad para abordar ese caso. La recolección del DOM no se preocupa por eso; una vez que la clase está en el elemento, este es recolectado. El escaneo del código fuente cubre las ramas que aún no se han ejecutado. La recolección del DOM abarca los nombres de clase que nunca se escribieron como literales. En un navegador sin proceso de compilación, la segunda opción es prácticamente obligatoria.

    Qué permite esa base y qué no

    Gracias a ese puente, un template al estilo shadcn-admin —con nombres de clase, variantes CVA y composición mediante cn()— puede renderizarse sin necesidad de instalación, sin servidor de desarrollo, con el motor real de Tailwind y la configuración real del proyecto.

    La ventaja no es que “la edición visual esté terminada”. Se trata de eliminar otro tipo de carga adicional.

    Abrir una plantilla de administración desconocida suele significar clonarla, instalarla, esperar, ejecutarla, hacer clic durante unos minutos y luego cerrarla. La mayor parte de ese tiempo lo ocupa la máquina. El mismo problema surge al mostrar a un compañero de equipo los tres estados de un componente, al verificar si el modo oscuro está listo o al analizar el espaciado de una tarjeta antes de proponer un cambio. Esas tareas toman segundos una vez que algo aparece en la pantalla.

    Ese costo de instalación y espera es lo que desaparece. Un proyecto shadcn puede utilizar sus clases reales en cuestión de segundos, y luego las funciones orientadas al código de Studio funcionan independientemente del sistema de estilos: selección bidireccional entre el código y la interfaz, acceso detallado a un componente definido en varios archivos, y la posibilidad de saber qué se romperá si se elimina ese archivo. Esas funciones operan sobre la estructura del código fuente, no sobre cómo se generó el CSS. Solo necesitan primero una renderización exitosa.

    Lo que sigue faltando es el inspector de estilos: className aún se edita como una propiedad de cadena sin formato, nada lee el mapa CVA para crear controles de variantes, y los tokens de tailwind.config llegan al motor pero no al panel. Las herramientas de MUI ya cuentan con edición estructurada de temas y sx; el lado de Tailwind aún no. Es mejor señalar esa brecha que dejar que una demostración dé una impresión diferente.

    El orden de ejecución es el verdadero problema, y es fácil pasarlo por alto desde afuera:

    Dado que Shadcn utiliza utilidades en lugar de propiedades de estilo para diseñar, es necesario incluir las cadenas de clases generadas en tiempo de ejecución en la página con CSS real antes de que cualquier inspector estructurado pueda editarlas. Hacerlo directamente en el navegador resulta complicado: el motor oficial monitorea el DOM ligero, mientras que el modo de diseño dibuja dentro de una raíz de sombra. Superar esta limitación al obtener las clases del DOM en tiempo real —y no de archivos— cierra esa brecha. Esa base finalmente está disponible.

    Lo que se coloca encima es más sencillo que la base, pero no trivial. Un mapa CVA es un objeto literal en un archivo local; leerlo y crear un selector de variante es una tarea habitual de interfaz de usuario. Sin embargo, escribir los cambios de vuelta no lo es. Un nombre de clase puede provenir al mismo tiempo de varios sitios de composición:

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

    Si un panel establece el relleno en 6, alguien debe decidir qué literal editar: la cadena base, la condicional o la definición de variante compartida utilizada por todos los demás botones. A veces no existe una respuesta única y correcta, solo una política. Ese problema de escritura de vuelta es el siguiente desafío difícil, y lo es de una manera distinta a como lo era el puente de estilo.

    Antes de comenzar el trabajo, habría sido fácil adivinar cuál de las dos partes era más complicada… y equivocarse.

    Los equipos que trabajan con shadcn y se preocupan por cómo debe comportarse esa superficie de control —desplegables de variantes gestionados por CVA, controles de espaciado que reescriben utilidades en lugar de estilos en línea, reglas que determinan qué literal debe ser modificado— son el público adecuado para dar forma a esa capa antes de su lanzamiento y no después.

    Lecturas relacionadas

  • Microfrontends de React con Federación de Módulos de Webpack — Sustituya las soluciones alternativas con iframe por la Federación de Módulos de Webpack 5 para que las aplicaciones React locales y remotas compartan un entorno de ejecución, se desplieguen de forma independiente y sigan pareciendo un único producto.
  • Patrones de grupos de botones Shadcn para UIs de React y Next.js — Explore más de 10 patrones de grupos de botones Shadcn: paginación, barras de herramientas, alternadores de moneda, entornos de despliegue, calificaciones, y dónde encaja cada uno en las aplicaciones React.