Tener tokens no evita la auditoría de dos semanas. He revisado sistemas con su capa de tokens montada y sincronizada con Figma donde un cambio de tono corporativo seguía costando cuarenta archivos tocados y una tanda de regresiones visuales en producción. Lo que fallaba en todos ellos era la distancia: entre el token y el componente había un solo salto.

Cuando un botón consume --color-brand-primary, el nombre está bien elegido, la variable existe y el acoplamiento es exactamente el mismo que con un hexadecimal escrito a mano. El día del rebranding hay que decidir, componente por componente, si ese «primario de marca» era el fondo de la acción principal, el borde de una tarjeta destacada o el color de un enlace. Tres decisiones distintas que compartían nombre, y ninguna de las tres está escrita en ninguna parte. Ahí es donde se van las dos semanas, no en el reemplazo.

La arquitectura que resuelve esto se organiza por preguntas antes que por tokens. Son cuatro, cada capa responde una, y ninguna puede responder la de otra.

Diagrama titulado «Estructura de tokens por capas». Cuatro planos apilados en perspectiva, de más claro abajo a más saturado arriba, conectados con cuatro filas. De abajo arriba: primitivo, con el token --color-brand-first-700; semántico, con --color-accent-700; rol, con --color-action-bg; y componente, con --button-color-bg.

Qué pregunta responde cada capa

En el sistema que usa esta web, la cadena que acaba pintando el fondo de un botón de acento son cuatro declaraciones repartidas en cuatro archivos:

/* 1. Primitivo: un valor y ningún contexto */
--p4-color-brand-first-100: #c6e070;

/* 2. Semántico: qué papel juega ese valor dentro de la paleta */
--p4-color-accent-100: var(--p4-color-brand-first-100);

/* 3. Rol: qué función cumple en la interfaz, en este tema */
--p4-color-action-accent-bg: var(--p4-color-accent-100);

/* 4. Componente: el ámbito exclusivo del botón */
--p4-button-color-bg

El primitivo responde «qué color es». Es la materia prima, vive en un único archivo y su valor está en que ninguna de sus declaraciones sabe para qué sirve. Un #c6e070 no es un fondo ni un borde ni un texto, así que la pregunta «cuántos verdes hay en producción» tiene respuesta exacta y verificable.

La capa semántica responde «qué es este color para la marca». --p4-color-accent-100 designa el acento del sistema, sea este lima hoy o el que decida la marca dentro de dos años, y no dice nada sobre dónde se usa. Todas sus declaraciones tienen la misma forma: un alias que apunta a un primitivo.

El rol responde «qué función cumple en la interfaz». --p4-color-action-accent-bg es el fondo de una acción de acento, y eso describe un comportamiento. Un botón, un enlace con aspecto de botón y una fila seleccionable comparten rol aunque no compartan componente.

Queda la capa de componente, que responde «y en este elemento en particular, qué». --p4-button-color-bg es la puerta de escape del botón y en este sistema no está declarada en ningún archivo: aparece solo como primer argumento de un var(), sin valor asignado, para que una página pueda sobrescribirla en un caso puntual sin forkear el componente. Su salud se mide por lo poco que aparece.

La capa de rol es la única que conmuta

Casi todos los sistemas de tres capas que he leído fusionan el semántico y el rol en un mismo nivel, porque desde fuera parecen la misma abstracción: los dos son «nombres con significado» frente a un hexadecimal. Separarlos tiene una justificación operativa, y se ve mirando qué archivo cambia cuando cambia algo.

La capa semántica es invariante. No depende del tema, ni del viewport, ni de la densidad de pantalla. El acento del sistema es el mismo en claro y en oscuro. La escala de neutros es la misma en móvil y en escritorio. Ese archivo se toca cuando cambia la marca, y solo entonces.

Los tokens de rol son lo contrario: existen para conmutarse. Este es el mismo rol en los dos temas del sistema:

:root, [data-theme='light'] {
  --p4-color-action-base-bg: var(--p4-color-neutral-970);
  --p4-color-action-base-fg: var(--p4-color-neutral-30);
}

[data-theme='dark'] {
  --p4-color-action-base-bg: var(--p4-color-neutral-30);
  --p4-color-action-base-fg: var(--p4-color-neutral-970);
}

Los dos bloques referencian los mismos dos tokens semánticos, intercambiados. Si el rol y el semántico vivieran en la misma capa, ese intercambio habría que escribirlo redefiniendo la paleta, que es justo lo que produce el resultado absurdo de un token llamado neutral-30 que en modo oscuro vale casi negro. He visto esa solución en producción más de una vez, y siempre acaba igual: nadie se fía del nombre de ningún token, así que se comprueba el valor computado en el inspector antes de usarlo.

La capa semántica y la de rol responden a ejes independientes. Una cambia con la identidad, la otra con el contexto de presentación. Separarlas cuesta un archivo y un salto más al depurar; fusionarlas cuesta la fiabilidad de los nombres, que es lo único que hace útil a un token.

La marca entra al sistema por una línea

De las cuatro capas, solo una toca la marca, y su superficie de contacto se puede señalar con el dedo. En este repositorio, el primitivo --p4-color-brand-first-100 lo referencia el archivo de tokens semánticos y nadie más. El semántico --p4-color-accent-100 lo referencian los dos archivos de tema y nadie más. Ningún componente menciona ninguno de los dos.

Cambiar el acento del sistema es reasignar ese alias a otro primitivo. El fondo del botón de acento cambia, su estado hover cambia porque pertenece a la misma familia de rol, la superficie de acento de las tarjetas cambia, y ningún archivo de componente se abre.

Después queda una sola comprobación manual, y es de contraste. El texto sobre el botón de acento está fijado a --p4-color-neutral-970 en los dos temas, no heredado del color de texto genérico, porque el par fondo/texto es una decisión única y no dos independientes. Con el lima actual da 13,88:1; con el texto claro del tema oscuro daría 1,41:1. Un acento nuevo obliga a rehacer esa verificación en los dos temas antes de dar el cambio por bueno, y son diez minutos.

El modo oscuro deja de ser un proyecto

El tema oscuro de este sistema es un archivo de reasignaciones a otros primitivos, sin una sola media query dentro de un componente y sin una sola condición en el código de la interfaz. Eso es el modo oscuro entero.

El mecanismo se extiende a cualquier eje nuevo. Hay un archivo de tokens por escalón de viewport, y la tipografía del botón cambia al cruzar un breakpoint por el mismo mecanismo por el que su color cambia al cruzar un tema. El componente escribe var(--p4-typo-action-m-font-size) y no sabe cuál de esos archivos ha ganado la cascada. Tema, resolución y viewport son ejes independientes tratados con una única mecánica: archivos que reasignan tokens que ya existen.

La capacidad multimarca es el mismo mecanismo aplicado a la capa semántica en lugar de a la de rol. Una marca nueva es un archivo de alias que apunta a otros primitivos, y la mitad superior del sistema no se entera.

Lo que cuesta la cuarta capa

Cuatro niveles de indirección tienen un precio y prefiero cuantificarlo antes de recomendarlos.

Depurar deja de ser inmediato. Averiguar por qué un botón se ve de un color concreto obliga a recorrer la cadena entera en el inspector hasta llegar al literal, y a quien llega nuevo al sistema hay que explicarle por qué el mismo #050505 tiene tres nombres distintos según a qué altura de la cadena se mire. En una sesión de depuración con prisa, eso son minutos reales.

Hay además una grieta que la arquitectura permite y que conviene vigilar. Algunos roles apuntan a otros roles en vez de a un token semántico:

--p4-color-action-ghost-fg: var(--p4-color-surface-base-fg);

Es deliberado, porque un botón fantasma tiene que tomar el color del texto de la superficie sobre la que se apoya, y esa relación es más estable que cualquier valor concreto. Pero rompe la regla de que cada capa solo referencia la inmediata inferior, y en un repositorio con más manos ese atajo se generaliza hasta que la capa de rol se convierte en un grafo en lugar de un nivel. Hoy lo sostiene la revisión, no una regla de lint, y esa es la deuda que tiene el montaje.

La otra excepción es más pequeña y también consciente: --p4-color-action-base-border: transparent es un literal escrito dentro de la capa de rol. Consideré crear un primitivo para transparent y mantener la pureza del modelo. Lo descarté porque un token que solo puede tener un valor no aporta capacidad de cambio, solo un salto más, y la coherencia del diagrama no es una razón suficiente para pagarlo.

Tres capas bastan más veces de lo que parece

La arquitectura de cuatro capas responde a un problema concreto, y fuera de ese problema es burocracia.

Con un producto único, una identidad de marca y dos personas de frontend, la cadena de primitivo a semántico a componente cubre todo lo que va a ocurrir en los próximos dos años. La disciplina que hay que sostener ahí no es arquitectónica sino de higiene: que ningún componente escriba un hexadecimal. Añadir la capa de rol en ese contexto genera un archivo más que mantener y una explicación más que dar en cada incorporación, a cambio de una flexibilidad que nadie va a ejercer.

La cuarta capa empieza a pagarse cuando aparece el segundo eje de variación. Dos temas es el caso más común y el más barato de detectar. Multimarca, multiplataforma con iOS y Android consumiendo la misma fuente de verdad, o un producto que se vende con la identidad visual del cliente son los casos donde omitirla solo aplaza el trabajo. La deuda se cobra el día que hay que introducir la capa en un sistema que ya tiene componentes escritos contra la anterior, y esa migración sí cuesta las dos semanas del principio.

El criterio que aplico para decidirlo cabe en una pregunta: ¿existe algún contexto en el que este mismo elemento deba verse distinto sin dejar de ser el mismo elemento? Si la respuesta es no, tres capas. Si la respuesta es sí y no hay capa de rol, el sistema ya está acumulando la deuda aunque todavía no se note.

La anatomía completa de la cadena, con la doble indirección que convierte al componente en una API y el caso de las islas de tema, está en Design tokens como única fuente de verdad.