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

Pirámide de abstracción de tokens semánticos

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 tokensbasecomponents.
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.