En organizaciones a gran escala con múltiples productos digitales (aplicaciones web, clientes móviles nativos de iOS/Android, widgets embebidos y paneles de administración), la falta de sincronización entre diseñadores en Figma e ingenieros frontend provoca una deuda visual crítica:
#3b82f6 frente a #2563eb), espaciados aleatorios (de 13px a 19px) y radios de borde caóticos.La respuesta arquitectónica a este reto es el concepto de Design Tokens (Tokens de diseño).
La arquitectura de tokens de diseño empresarial se fundamenta en una estricta jerarquía de tres niveles:
color.blue.500 = #3b82f6, space.4 = 16px).surface.canvas = var(--color-slate-950), color.interactive = var(--color-blue-500)).button.primary.bg = var(--color-interactive)).En Tailwind CSS v4, la arquitectura de estilos evolucionó hacia el motor nativo Lightning CSS en Rust, suprimiendo por completo el archivo tailwind.config.js en favor de la directiva @theme y variables en cascada CSS Custom Properties.
Los componentes nunca deben consumir tokens primitivos directamente (como bg-blue-500). Consumen exclusivamente alias semánticos (bg-brand-primary). En Tailwind v4, la directiva @theme enlaza las variables generadas por Style Dictionary directamente con las clases de utilidad sin sobrecarga de JavaScript en tiempo de ejecución.
La canalización de sincronización de tokens enlaza la única fuente de verdad en Figma con los componentes React mediante un flujo automatizado:
┌─────────────────────────────────────────────────────────────┐
│ SISTEMA DE DISEÑO EN FIGMA (Variables) │
│ Modos: Light / Dark / High Contrast / Tenant Brand │
└──────────────────────────────┬──────────────────────────────┘
│ Exportación vía plugin W3C Tokens Studio
▼
┌─────────────────────────────────────────────────────────────┐
│ REPOSITORIO DE DESIGN TOKENS (DTCG JSON) │
│ tokens/primitives.json | tokens/semantic-dark.json │
└──────────────────────────────┬──────────────────────────────┘
│ Compilación vía Style Dictionary v4
▼
┌─────────────────────────────────────────────────────────────┐
│ VARIABLES CSS Y TIPOS GENERADOS │
│ dist/variables.css | dist/tokens.d.ts │
└──────────────────────────────┬──────────────────────────────┘
│ Inyección en la capa CSS
▼
┌─────────────────────────────────────────────────────────────┐
│ TAILWIND CSS v4 (Directiva @theme) │
│ @theme { │
│ --color-canvas: var(--color-semantic-surface-canvas); │
│ --color-brand-primary: var(--color-semantic-brand); │
│ } │
└──────────────────────────────┬──────────────────────────────┘
│ Tipado estricto mediante CVA
▼
┌─────────────────────────────────────────────────────────────┐
│ COMPONENTES UI TIPADOS DE REACT 19 (Button, Card)│
│ <Button variant="primary" size="lg">Submit</Button> │
└─────────────────────────────────────────────────────────────┘Establecimiento de colores globales, retícula de espaciado (4px/8px grid) y radios en formato W3C DTCG JSON.
Mapeo de roles semánticos (surface, text, border, brand) vinculados a modos de tema (Light, Dark, High-Contrast).
Generación de CSS Custom Properties (variables.css) y declaraciones de tipos TypeScript para el monorrepositorio.
Inclusión de las variables generadas a través de la directiva @theme en el archivo principal globals.css.
Creación de componentes polimórficos con seguridad de tipos (Button, Card, Input) compatibles con el prop asChild.
¿Por qué está estrictamente prohibido en un kit de UI empresarial hacer referencia directa a tokens primitivos (por ejemplo, className='bg-violet-500')?
El archivo JSON fuente se estructura según la especificación del Design Tokens Community Group:
{
"color": {
"primitive": {
"violet": {
"500": { "$value": "#8b5cf6", "$type": "color" },
"600": { "$value": "#7c3aed", "$type": "color" },
"900": { "$value": "#4c1d95", "$type": "color" }
},
"slate": {
"900": { "$value": "#0f172a", "$type": "color" },
"950": { "$value": "#020617", "$type": "color" }
}
},
"semantic": {
"surface": {
"canvas": {
"$value": "{color.primitive.slate.950}",
"$type": "color"
}
},
"brand": {
"primary": {
"$value": "{color.primitive.violet.500}",
"$type": "color"
},
"hover": {
"$value": "{color.primitive.violet.600}",
"$type": "color"
}
}
}
}
}El archivo de configuración compila los tokens JSON en CSS Custom Properties conservando la integridad de referencias cruzadas:
import StyleDictionary from 'style-dictionary';
const sd = new StyleDictionary({
source: ['tokens/**/*.json'],
platforms: {
css: {
transformGroup: 'css',
buildPath: 'src/tokens/dist/',
files: [
{
destination: 'variables.css',
format: 'css/variables',
options: {
outputReferences: true, // Mantener referencias var() entre tokens
},
},
],
},
ts: {
transformGroup: 'js',
buildPath: 'src/tokens/dist/',
files: [
{
destination: 'tokens.ts',
format: 'javascript/es6',
},
{
destination: 'tokens.d.ts',
format: 'typescript/es6-declarations',
},
],
},
},
});
await sd.buildAllPlatforms();Las variables CSS generadas se vinculan a la directiva @theme en la hoja de estilos raíz de la aplicación:
@import "tailwindcss";
@import "../tokens/dist/variables.css";
/* Registro de tokens de diseño en el motor de utilidades de Tailwind v4 */
@theme {
--color-canvas: var(--color-semantic-surface-canvas);
--color-brand-primary: var(--color-semantic-brand-primary);
--color-brand-hover: var(--color-semantic-brand-hover);
--font-sans: "Inter Variable", -apple-system, BlinkMacSystemFont, sans-serif;
--radius-badge: 0.375rem;
}
/* Modificaciones de variables para el tema oscuro */
:root[data-theme="dark"] {
--color-semantic-surface-canvas: #09090b;
--color-semantic-brand-primary: #a855f7;
--color-semantic-brand-hover: #9333ea;
}
/* Modificaciones para el modo de alto contraste (WCAG AAA) */
:root[data-theme="high-contrast"] {
--color-semantic-surface-canvas: #000000;
--color-semantic-brand-primary: #ffff00;
--color-semantic-brand-hover: #ffffff;
}El diseñador ajusta el color de marca en Figma. El plugin exporta un archivo tokens.json estandarizado.
Valida referencias entre tokens y compila variables.css y tokens.d.ts de TypeScript.
El motor en Rust genera clases utilitarias (bg-brand-primary, text-canvas) a partir de variables CSS.
El componente Button aplica variantes estrictas de props (variant, size) tipadas con CVA.
¿Cómo se evita el destello de tema sin estilos (Flash of Unstyled Theme - FOUT) durante el renderizado en servidor en Next.js App Router?
A continuación se muestra un componente de botón de nivel empresarial que utiliza tokens semánticos de Tailwind v4, la utilidad class-variance-authority (CVA) y el patrón de slots (Slot / asChild) de Radix UI para polimorfismo seguro:
import React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { Slot } from '@radix-ui/react-slot';
export const buttonVariants = cva(
// Estilos base: layout, transiciones, anillos de foco, accesibilidad
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
primary: 'bg-brand-primary text-white hover:bg-brand-hover shadow-sm',
secondary: 'bg-bg-elevated border border-border text-text-primary hover:bg-bg-elevated/80',
outline: 'border border-brand-primary/50 text-brand-primary hover:bg-brand-primary/10',
ghost: 'text-text-muted hover:text-text-primary hover:bg-bg-elevated/50',
danger: 'bg-danger text-white hover:bg-danger/90',
},
size: {
sm: 'h-8 px-3 text-xs',
md: 'h-10 px-4 py-2',
lg: 'h-12 px-6 text-base',
icon: 'h-10 w-10 p-0',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
/**
* Con asChild=true el componente delega estilos y listeners en su primer hijo (ej. Link de Next.js)
*/
asChild?: boolean;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Component = asChild ? Slot : 'button';
return (
<Component
className={buttonVariants({ variant, size, className })}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';Utilizar utilidades arbitrarias como className="bg-[#8b5cf6]" en lugar de tokens semánticos como className="bg-brand-primary". Dicho componente permanecerá en un color violeta chillón en temas oscuros o de alto contraste, quebrando la coherencia de la interfaz.
// ❌ Antipatrón: el botón no se adapta al cambiar de tema
<button className="bg-[#8b5cf6] text-white px-4 py-2">Guardar</button>
// ✅ Solución correcta: clase semántica vinculada a variables de tema
<button className="bg-brand-primary text-white px-4 py-2">Guardar</button>Utilizar tokens primitivos en componentes (por ejemplo, bg-violet-500 en lugar de bg-brand-primary). Si la compañía decide migrar el color primario de violeta a esmeralda, habrá que modificar miles de archivos en vez de actualizar una única línea en el registro de tokens semánticos.
Si el tema preferido se lee desde localStorage dentro de un hook del cliente useEffect, el servidor siempre renderizará el tema claro y el cliente conmutará 100 ms después, generando un destello blanco molesto.
Solución: Guardar el tema en cookies y fijar <html data-theme="..."> en el servidor durante el SSR, o inyectar un script bloqueante minúsculo en <head>.
El polimorfismo mediante <Button as="a" href="/home"> rompe la inferencia de tipos de TypeScript (el compilador no puede determinar qué atributos HTML son válidos para button o anchor). El patrón asChild basado en @radix-ui/react-slot garantiza el 100% de seguridad de tipos en el elemento destino.
tailwind.config.js por la directiva nativa @theme impulsada por Lightning CSS en Rust.class-variance-authority) garantiza tipado estricto en todas las combinaciones de variantes en TypeScript.asChild mediante @radix-ui/react-slot proporciona polimorfismo seguro preservando la semántica del HTML.