Tokens semánticos: Deja de nombrar colores, empieza a nombrar intenciones

Si tu interfaz cambia de color como una discoteca cada vez que marketing respira, no es “porque diseño es subjetivo”. Es porque llamas a las cosas por lo que son, no por lo que significan. Un token semántico no es una variable bonita: es un contrato operacional entre diseño, código y negocio. Nombra el porqué de un valor, y tus componentes obedecen. Nómbralo por el qué, y cada sprint será una cacería de hexadecimales. Lee esto como un manifiesto, no como recetario.
Acto I — La mentira de la medianía
Dogma reproducido
“Los tokens son variables en :root
. Pones --primary: #0d6efd;
y listo.”
Autopsia
Producto realista: SaaS B2B con “modo oscuro” y campañas temporales. Síntomas visibles: tres botones “primarios” con tres azules distintos; el fondo del modal no respeta accesibilidad; el “tema Black Friday” se come el hover del CTA. Causa raíz: variables con nombres de valor crudo (–blue-500, –gray-900) usadas directamente en componentes, sin capa semántica intermedia. Cadena de decisiones: “declara colores crudos en \:root
” → “consúmelos en componentes” → “cuando cambie la marca, ya veremos” → parches locales por campaña → deuda y regresiones.
Evidencia mínima
• 3,5 h por cambio de color global del CTA (buscar y reemplazar en 14 componentes).
• +22 KB de CSS redundante tras dos campañas (“Black Friday”, “Spring”).
• p95 INP empeora un 8% por recalcular estilos en componentes temados a mano.
• 2 bugs de contraste AA en formularios (soporte los detecta con usuarios en dark mode).
Smells
• Variables con nombres de valor (–blue-500, –font-32px) usadas directamente en componentes.
• Overrides por componente para “tema” en lugar de redefinir un set semántico.
• Comentarios tipo “este azul es el de botones” pegados a valores crudos (documentación viva en comentarios = documentación muerta).
Contraejemplo rápido (antes → después, 5 líneas)
Antes (valor crudo en componentes, 5 líneas):
CSS
.btn-primary { background: #0d6efd; }
.btn-primary:hover { background: #0b5ed7; }
.modal { background: #111; }
.link { color: #0d6efd; }
.badge-sale { background: #ff006e; }
Después (rol semántico + alias, 5 líneas):
CSS
.btn-primary { background: var(--color-accent-primary); }
.btn-primary:hover { background: var(--color-accent-primary-hover); }
.modal { background: var(--color-surface-elevated); }
.link { color: var(--color-link); }
.badge-sale { background: var(--color-promo); }
Regla de salida del Acto I: si reconoces tu repo, ya ves el coste en euros: tiempo de cambio lento, más CSS del necesario, y riesgo a11y. Repasa custom properties en MDN y entiende por qué un alias semántico es tu póliza.
Acto II — La verdad
Visión
Tokens semánticos = nombres de guerra. No describen el pigmento; describen la función. Son una capa de alias que mapea las decisiones crudas de diseño a roles de interfaz estables y auditables. Con tokens, la marca cambia de piel sin cirugía a corazón abierto.
Modelo operativo (diagrama verbal)
• Escalas crudas → colores/espacios/tipos en rangos neutrales.
• Alias semánticos → --color-accent-primary
, --space-section
, --font-heading-xl
.
• Capas → @layer tokens → base → components.
• Contextos → temas (data-theme
), campañas (data-campaign
), accesibilidad (alto contraste).
• Gobernanza → checklist, métricas y coto de caza.
Tácticas de campo (con no-uso)
1. Diseña la taxonomía semántica
— Qué: define un vocabulario por roles: color-*
, surface-*
, text-*
, space-*
, radius-*
, shadow-*
, font-\*
.
— Por qué: orientación por propósito, no por pigmento.
— No usar: si el proyecto es un prototipo desechable de 1 pantalla y 1 semana.
2. Alía valores crudos → roles semánticos
— Qué: --color-accent-primary: var(--blue-600)
, no hex directo en componentes.
— Por qué: cambios de marca/campaña sin tocar componentes.
— No usar: si el valor es experimental y no quieres comprometer semántica (usa sandbox local).
3. Contextualiza por atributo del root
— Qué: \:root
, \:root
.
— Por qué: el mismo componente hereda el contexto, sin duplicar CSS.
— No usar: si el estado es local al módulo (entonces usa el contenedor, no el root; ver contenedores CSS).
4. Automatiza pruebas de contraste y regresión de tokens
— Qué: pipeline que evalúe ratios AA/AAA con paletas renderizadas por tokens.
— Por qué: reduces bugs a11y y demandas de soporte.
— No usar: nunca; aquí no hay excusa. Si dudas, repasa WCAG contraste mínimo.
5. Versiona sets de tokens, no parches sueltos
— Qué: un PR cambia tokens y demuestra impacto con captura de métricas.
— Por qué: trazabilidad del “por qué cambió la interfaz”.
— No usar: si la variación es por experimentación A/B aislada (gestión en capa de experimento).
Demostración mínima (10 minutos replicable)
1. Declara capas y escalas crudas:
CSS
@layer tokens,base,components;
@layer tokens { :root {
--blue-600: #155ee7; --blue-700: #114ec0;
--gray-025: #f9fafb; --gray-900: #0b0f14;
--spacing-4: 1rem; --radius-2: 8px;
} }
2. Crea alias semánticos:
CSS
@layer tokens { :root {
--color-accent-primary: var(--blue-600);
--color-accent-primary-hover: var(--blue-700);
--color-surface-elevated: var(--gray-025);
--color-text-primary: #111;
--space-section: var(--spacing-4);
--radius-card: var(--radius-2);
} }
3. Aplica en componentes desde base/components:
CSS
@layer components {
.btn-primary { background: var(--color-accent-primary); color: #fff; }
.card { background: var(--color-surface-elevated); border-radius: var(--radius-card); padding: var(--space-section); }
.link { color: var(--color-accent-primary); }
}
4. Tema oscuro y campaña sin tocar componentes:
CSS
@layer tokens {
:root[data-theme=dark] {
--color-surface-elevated: var(--gray-900);
--color-text-primary: #f4f5f6;
}
:root[data-campaign=bf] {
--color-accent-primary: #ffb703;
--color-accent-primary-hover: #f6a300;
}
}
5. Mide salida: cambio global de CTA < 5 min; bytes CSS −15%; 0 overrides locales. Si necesitas herramienta externa, estudia el formato del grupo del W3C: Design Tokens Format (draft) y considera cómo integrarlo en build.
Trade-offs
• Curva inicial de nomenclatura y acuerdos con diseño.
• Disciplina para no “colar” hex en componentes.
• Gobernanza: hace falta checklist y linter que grite cuando te sales del carril.
Impacto en negocio
• Time-to-change de campaña a horas → minutos.
• Coherencia de marca consistente entre plataformas (web/app/email) usando el mismo set semántico.
• Menos regresiones a11y: los contrastes se controlan a nivel de token, no de componente. Si dudas, repasa cómo testear contraste con tokens.
Regla de salida del Acto II
Si un junior sigue estos pasos hoy, reproduce el resultado: tema nuevo sin tocar componentes, métricas registradas y sin !important. Si no es reproducible, era opinión, no doctrina.
Acto III — El manifiesto
Principios no negociables
• Nombra por función, no por pigmento: –color-accent-primary, nunca –blue-600 en componentes.
• Los tokens viven en @layer tokens; los componentes solo consumen.
• Un rol, un origen: cada token semántico apunta a un único valor crudo por contexto.
• Los contextos son atributos del root (data-theme, data-contrast, data-campaign).
• Prohibido el hex directo fuera de tokens/utilities.
• Todo PR con tokens incluye antes/después y una métrica.
• Los tokens de espacio gobiernan el ritmo; márgenes “decorativos” quedan fuera.
• Contraste AA mínimo garantizado por diseño del set (test automático).
• Nomenclatura estable: prefijos por dominio (color, text, surface, space, radius, shadow).
• Documentación viva: tabla de tokens generada en build, no en Confluence.
Definición de Hecho (DoD) verificable
• No hay hex ni rgb() directos en componentes del diff.
• Especificidad media del diff ≤ 0-1-1 (usa regla de especificidad).
• Los cambios de color/tipo/espacio se realizan redefiniendo tokens, no componentes.
• Se adjunta captura de contraste AA/AAA para text-primary y link en los contextos activos.
• La página de catálogo de tokens se regenera mostrando los cambios.
• 0 !important fuera de utilities de a11y (focus ring, skip-link).
Métricas de guardarraíl
• CSS total por vista ≤ 60 KB gzip (objetivo 45 KB).
• p75 Time-to-change de color de marca ≤ 10 min del commit a producción.
• Especificidad media del proyecto en 0-1-0 / 0-1-1.
• % de componentes que consumen solo tokens ≥ 95%.
• 0 fallos de contraste AA en rutas críticas (home, checkout, forms).
Coto de caza (anti-patrones prohibidos)
• –blue-500 usado directamente en un componente. Justificación: rompe el contrato semántico.
• Overrides de campaña dentro de componentes. Justificación: contexto debe vivir en root.
• Tokens con nombres de valor (–font-32px) o ambiguos (–brand). Justificación: semántica difusa.
• !important para “arreglar” colisiones de tema. Justificación: deuda silenciosa.
• Variables sin prefijo de dominio (p. ej., –primary), que no escalan a múltiples superficies.
Plan de acción de 24 h
1. Inventario: extrae todos los colores/tamaños de fuente/espacios usados en producción (script + tabla).
2. Normaliza escalas crudas y crea @layer tokens con ellas (paletas, tipografías, espacios).
3. Define los alias semánticos mínimos para rutas críticas: color-accent-primary, color-surface-elevated, text-primary, space-section, radius-card.
4. Reemplaza en 3 componentes clave para demostrar “antes/después” y mide bytes/tiempo.
5. Añade contextos data-theme dark y data-contrast high; ejecuta test AA. Documenta en doctrina de tokens.
Ejemplos quirúrgicos
1) Alias semántico + tema oscuro + campaña
CSS
@layer tokens,components;
@layer tokens { :root {
--blue-600: #155ee7; --blue-700: #114ec0;
--yellow-600: #ffb703; --surface-0: #ffffff; --surface-900: #0b0f14;
--color-accent-primary: var(--blue-600);
--color-accent-primary-hover: var(--blue-700);
--color-surface-elevated: var(--surface-0);
--color-text-primary: #111;
} }
@layer tokens { :root[data-theme=dark] {
--color-surface-elevated: var(--surface-900);
--color-text-primary: #f2f4f6;
} }
@layer tokens { :root[data-campaign=bf] {
--color-accent-primary: var(--yellow-600);
} }
@layer components {
.btn-primary { background: var(--color-accent-primary); color: #fff; }
.btn-primary:hover { background: var(--color-accent-primary-hover); }
.card { background: var(--color-surface-elevated); color: var(--color-text-primary); }
}
2) Catálogo accesible de tokens (HTML)
HTML <section id="tokens" aria-label="Catálogo de tokens">
<article class="swatch" style="--swatch: var(--color-accent-primary)">Accento primario</article>
<article class="swatch" style="--swatch: var(--color-surface-elevated)">Superficie elevada</article>
<a class="fuchsia" href="/catalogo-tokens">Ver tabla completa</a>
</section>
3) Pipeline de contraste con tokens (pseudocódigo JS)
JS
/* evalúa AA para text-primary sobre cada surface definida por tokens */
const pairs = [['--color-text-primary','--color-surface-elevated'],['--color-link','--color-surface-elevated']];
for (const [fg,bg] of pairs) { assert(contrast(getVar(fg), getVar(bg)) >= 4.5); }
/* integra con CI y falla si baja del umbral */
Si quieres referencias para profundizar, además de MDN y el borrador de formato del W3C, mira los fundamentos de WCAG en MDN y cómo mapear tokens a plataformas en semantic color en Material (inspírate, no lo copies).
Epílogo — de variables a criterio
Los tokens semánticos no te hacen mejor diseñador ni mejor front. Te obligan a pensar. A nombrar el propósito. A blindar la experiencia contra la deriva del “ya tal”. Cuando el próximo “rebranding” llegue un viernes por la tarde, decidirás si eres quien corre a cambiar hex en 40 ficheros o quien mueve un set semántico y se va a la cerveza. Si has llegado hasta aquí, ya sabes cuál de los dos eres.