El primer sistema de diseño que construí salió con exactamente un error que persiguió a todo lo demás después: usábamos blue-500 directamente en el código de los componentes. Los botones eran blue-500. Los enlaces eran blue-500. El anillo de foco era blue-500. Se sentía limpio y DRY. Luego marketing rehízo la marca de azul a verde azulado, y nos pasamos dos sprints persiguiendo cada referencia hex literal a lo largo de cuarenta repositorios, y aun así se nos escaparon unas cuantas, porque algunas se habían codificado como #3B82F6 en lugar de la variable. Todo ese episodio podría haber sido un cambio de una sola línea. La razón por la que no lo fue es la razón entera por la que existen los tokens de color de sistema de diseño.
Un token de color no es más que una referencia con nombre a un valor de color. Pero el valor de los tokens no está en el nombre: está en las capas de nombres, y en ser disciplinado sobre qué capa tiene permitido tocar un componente. Acierta con la estratificación y un rebranding, un lanzamiento de modo oscuro o una corrección de contraste se convierten en un cambio pequeño y contenido. Falla y obtienes la limpieza de cuarenta repositorios.
Tres capas: primitiva, semántica, de componente
El modelo mental que ha aguantado en cada sistema en el que he trabajado es una jerarquía de tres niveles. Cada nivel referencia al de debajo, y los componentes solo tienen permitido consumir el nivel superior.
- Los tokens primitivos (también llamados base o globales) son tu paleta en bruto.
blue-500: #3B82F6,gray-900: #111827,red-600: #DC2626. Describen qué es el color y nada sobre dónde se usa. Un primitivo nunca debería aparecer directamente en el CSS de un botón. - Los tokens semánticos describen la intención: el trabajo que hace el color.
color-text-primary,color-bg-surface,color-border-default,color-action-primary,color-feedback-error. Un token semántico apunta a un primitivo:color-action-primary → blue-600. - Los tokens de componente son opcionales y describen una parte concreta de un componente concreto.
button-primary-bg,card-border-color,input-focus-ring. Apuntan a tokens semánticos, no a primitivos.
Aquí está la regla que hace que todo funcione, y la que la mayoría de los equipos se saltan: los componentes referencian tokens semánticos o de componente, nunca primitivos. En el momento en que un botón se salta la capa semántica para agarrar blue-500 directamente, has reintroducido el problema de los cuarenta repositorios, un componente cada vez.
¿Por qué molestarse en absoluto con la capa semántica intermedia? Porque es la costura donde ocurre el theming. Cuando cambias a modo oscuro, no repintas cada componente: reapuntas unas pocas docenas de tokens semánticos a primitivos distintos. Los componentes no se enteran de que algo ha cambiado. Esa indirección es la decisión de mayor apalancamiento de todo el sistema.
Construye la escala antes de nombrar nada
Antes de la semántica, necesitas primitivos a los que valga la pena apuntar, y eso significa una escala numérica, convencionalmente del 50 al 950. La convención popularizada por Tailwind (50, 100, 200 … 900, 950) se ha convertido de hecho en el estándar de la industria, y vale la pena adoptarla aunque solo sea para que los nuevos ingenieros se sientan en casa. 50 es el tinte más claro, 500 es aproximadamente el tono de marca puro, 950 es casi negro.
El error que cometen los principiantes es generar una escala aclarando y oscureciendo ingenuamente en sRGB: añadiendo blanco para los tintes, negro para las sombras. Obtienes medios tonos enturbiados y pasos que no se sienten espaciados de forma uniforme, porque la luminosidad percibida no es lineal en RGB. Un 400 acaba pareciendo casi idéntico a un 500, mientras que de 700 a 800 hay un precipicio. La solución es espaciar tus pasos en un modelo perceptual. Las herramientas que trabajan en HSL te llevan la mayor parte del camino; las que usan OKLCH (ahora bien soportado en CSS) te llevan el resto, porque OKLCH mantiene la luminosidad percibida consistente a medida que cambia el tono. Si quieres juzgar a ojo tintes y sombras armoniosos rápidamente mientras prototipas una escala, el generador de paletas de color es una forma veloz de ver las relaciones entre tonos antes de comprometerlos en tokens.
Unas cuantas reglas ganadas a pulso para las escalas:
- Ancla tu escala al contraste, no a la estética. Decide pronto qué paso es tu color de "texto sobre blanco" y verifica que alcanza las ratios que necesitas. En la mayoría de las rampas neutras, el
600o el700es donde cruzas el 4,5:1 frente al blanco. Saber eso te permite escribir reglas semánticas como "el texto del cuerpo usagray-700" con confianza. - Mantén el mismo número de pasos en todos los tonos. Si el azul tiene un
950y el verde se detiene en900, tus mapeos semánticos se vuelven irregulares y el modo oscuro se convierte en una pesadilla de casos especiales. - No sobregeneres. Once pasos por tono es de sobra. He visto equipos sacar escalas de veinte pasos donde la mitad de los pasos son visualmente indistinguibles y nadie usa nunca el
425.
Nomenclatura: describe el trabajo, no la apariencia
El principio de nomenclatura más importante es este: los tokens semánticos y de componente deben nombrar el propósito, nunca la apariencia. Un token llamado color-text-red se rompe el día en que el texto de error pasa a ser naranja: el nombre ahora miente. Un token llamado color-feedback-error sobrevive a ese cambio intacto, porque "error" sigue siendo cierto independientemente del rojo (o naranja) al que resuelva.
Una estructura de nomenclatura que escala se lee de la categoría general al modificador específico, porque ordena y audita de forma limpia:
categoria-propiedad-variante-estadocolor-bg-surface,color-bg-surface-raised,color-bg-surface-sunkencolor-action-primary,color-action-primary-hover,color-action-dangercolor-text-primary,color-text-secondary,color-text-disabled,color-text-on-action
Ese último —color-text-on-action— es un token que la gente olvida y lamenta. Cuando tu color de acción es un azul saturado, el texto sobre ese azul necesita ser blanco o casi blanco, y ese emparejamiento no tiene nada que ver con color-text-primary. Conviértelo en un token explícito o verás a los ingenieros codificar a fuego #FFFFFF sobre los botones y luego abrir bugs de accesibilidad cuando alguien introduzca una acción de "advertencia" amarillo claro.
Para los primitivos, nombrar la apariencia es correcto y esperado —blue-500 debería describir lo que es, porque ese es todo el sentido de la capa primitiva—. La regla de "describir la intención" se aplica por encima de ella.
Implementación con propiedades personalizadas CSS
Las propiedades personalizadas CSS son el hogar natural de los tokens porque cascadean y pueden ser sobrescritas por el contexto, que es exactamente cómo funciona el theming. El patrón son dos capas de variables: los primitivos definidos una vez en la raíz, y los semánticos definidos en la raíz y redefinidos bajo un selector de tema.
Define los primitivos globalmente: --blue-600: #2563EB, --gray-900: #111827, y así sucesivamente. Luego mapea los semánticos a ellos: --color-action-primary: var(--blue-600) y --color-text-primary: var(--gray-900). Los componentes solo leen las variables semánticas: color: var(--color-text-primary). Nunca nombran un primitivo y nunca nombran un código hex.
En Tailwind v4 esto se mapea limpiamente sobre la nueva directiva @theme, que tanto declara los tokens como genera utilidades a partir de ellos, exponiendo cada token como una variable CSS en tiempo de ejecución. Defines --color-text-primary: var(--color-gray-900) dentro de @theme, usas text-primary en el marcado y sobrescribes la variable bajo un selector .dark en tu capa base. La documentación oficial de variables de tema de Tailwind cubre la mecánica, y la misma disciplina de dos capas se aplica independientemente del framework.
El modo oscuro es un problema de mapeo de tokens, no de color
La razón por la que la capa semántica se gana su sustento es que el modo oscuro se vuelve casi trivial una vez que existe. No tocas un solo componente. Sobrescribes los tokens semánticos dentro de un ámbito .dark (o [data-theme="dark"]) para que resuelvan a primitivos distintos.
En modo claro, --color-bg-surface: var(--white) y --color-text-primary: var(--gray-900). En modo oscuro, bajo el selector .dark, --color-bg-surface: var(--gray-900) y --color-text-primary: var(--gray-100). Cada componente que lee color-bg-surface se voltea automáticamente, porque la cascada hace el trabajo.
Dos cosas que muerden a la gente aquí:
- No te limites a invertir la escala. El texto blanco puro sobre negro puro (
#000sobre#FFFinvertido) es duro y provoca halación: ese resplandor borroso alrededor del texto en pantallas OLED. Las superficies oscuras deberían ser un gris oscuro como#121212a#1A1A1A, y tu texto más brillante un blanco roto suave en lugar de#FFFFFF. Por esto tu escala necesita un950y un50reales en lugar de negro y blanco literales en los extremos. - Desatura los acentos para los fondos oscuros. Un
blue-600que se ve seguro sobre blanco puede vibrar incómodamente sobre una superficie casi negra. Los temas oscuros suelen apuntar su token de acción a un paso más claro y ligeramente menos saturado:blue-400en lugar deblue-600. Como esto es un único remapeo de token, puedes ajustarlo globalmente en segundos.
Las trampas que de verdad se rompen a escala
Tras suficientes rebrandings y lanzamientos de temas, los modos de fallo riman:
- Filtrar primitivos a los componentes. El pecado original. Pásale el linter: una regla que prohíba los nombres de tokens primitivos y el hex en bruto en los archivos de componentes vale la pena escribirla el primer día.
- Tokens semánticos que en secreto codifican un valor.
color-blue-actiones un primitivo disfrazado de semántico. Si el nombre contiene un tono, no es realmente semántico, y se romperá en el rebranding. - Saltarse la capa semántica "para ir más rápido". Los componentes enlazados directamente a los primitivos se sienten bien hasta tu primer tema. Entonces estás refactorizando bajo presión de plazos, el momento más caro para hacerlo.
- Probar el contraste solo en modo claro. Un emparejamiento que pasa el 4,5:1 sobre blanco puede fallar sobre una superficie oscura, y viceversa. Cada par semántico de texto sobre fondo necesita comprobarse en cada tema. Los umbrales de la WCAG son 4,5:1 para el texto normal y 3:1 para el texto grande y los componentes de interfaz, según la orientación de contraste de la WCAG del W3C, y tu sistema de tokens es donde deberías incorporar esas garantías, no donde deberías descubrir las violaciones.
- Demasiados tokens. Un sistema con cuatrocientos tokens semánticos es tan inutilizable como uno con cuatro. Si dos tokens siempre resuelven al mismo valor y siempre lo harán, son un solo token. Añade especificidad solo cuando aparezca una divergencia real.
El resumen honesto es que los tokens no toman las decisiones de color por ti: hacen que las decisiones de color sean baratas de cambiar. La estructura es lo que te compra la capacidad de rehacer la marca un viernes, lanzar el modo oscuro en un sprint y corregir un bug de contraste en una línea en lugar de en cuarenta repositorios. Invierte tu esfuerzo en la capa semántica y en la nomenclatura, mantén a los componentes honestos sobre qué nivel tocan, y el sistema absorberá cambios que ni siquiera puedes predecir todavía.
Preguntas frecuentes
¿Cuál es la diferencia entre los tokens de color primitivos y semánticos?
Un token primitivo (o base) nombra un valor de color en bruto sin contexto, por ejemplo blue-500: #3B82F6. Describe qué es el color. Un token semántico nombra el propósito del color, como color-action-primary o color-feedback-error, y apunta a un primitivo. Los componentes deberían referenciar tokens semánticos, nunca primitivos, de modo que un rebranding o un theming solo requiera reapuntar la capa semántica en lugar de editar cada componente.
¿Cómo debería nombrar los tokens de color en un sistema de diseño?
Nombra los tokens semánticos y de componente por intención, no por apariencia. color-feedback-error sobrevive a un rediseño en el que el rojo de error pasa a ser naranja; color-text-red no. Usa una estructura de lo general a lo específico como categoria-propiedad-variante-estado (p. ej. color-bg-surface-raised, color-action-primary-hover). Los tokens primitivos son la excepción: deberían describir el color literal, como gray-900, porque ese es todo su propósito.
¿Por qué usar una escala numérica de color del 50 al 950?
La escala del 50 al 950 (50, 100, 200 … 900, 950) popularizada por Tailwind se ha convertido en un estándar de la industria, así que resulta instantáneamente familiar para los nuevos ingenieros. 50 es el tinte más claro, ~500 es el tono de marca puro y 950 es casi negro. Espaciar los pasos en un modelo perceptual como HSL u OKLCH —en lugar de mezclar ingenuamente blanco y negro en sRGB— mantiene los pasos con una sensación de espaciado uniforme y evita los medios tonos enturbiados.
¿Cómo facilitan el modo oscuro los tokens de color?
El modo oscuro pasa a ser un problema de mapeo de tokens en lugar de un rediseño. Como los componentes solo leen tokens semánticos, sobrescribes esos tokens dentro de un ámbito .dark o [data-theme=dark] para que resuelvan a primitivos distintos; por ejemplo color-bg-surface apunta a blanco en modo claro y a gray-900 en oscuro. La cascada CSS voltea cada componente automáticamente, sin cambios en el código del componente. Recuerda desaturar los colores de acento y evitar los fondos negro puro para reducir la fatiga visual y la halación.
¿Qué ratios de contraste deben cumplir los tokens de color para la accesibilidad?
La WCAG exige una ratio de contraste de al menos 4,5:1 para el texto normal y 3:1 para el texto grande y los componentes de interfaz. Incorpora estas comprobaciones a tu sistema de tokens validando cada emparejamiento semántico de texto sobre fondo en cada tema. Una combinación que pasa en modo claro puede fallar en modo oscuro, así que prueba ambos, y define un token explícito de texto sobre acción para garantizar que las etiquetas sobre botones saturados sigan siendo legibles.
¿Quieres experimentar con los colores?
Prueba nuestro generador gratuito de paletas de color para encontrar tu armonía perfecta, con un comprobador de contraste WCAG integrado.
Abrir el generador