Volver al Blog
UX/UI Design17 de diciembre de 202511 min de lectura

Design Tokens con CSS Custom Properties. De Figma al Código

DR

Doug Ribeiro

11 min de lectura

Los design tokens son la decisión estructural de mayor impacto en cualquier design system en el que he trabajado. No son una tendencia ni una funcionalidad de Figma. Son la capa contractual entre diseño y código. Cuando se hacen bien, un cambio de color en Figma se propaga a producción en minutos. Cuando se hacen mal, pasas semanas buscando valores hexadecimales hardcodeados. Este post recorre el flujo de trabajo completo que uso: desde Figma Variables hasta CSS custom properties hasta componentes Angular, incluyendo temas.

Qué Son Realmente los Design Tokens

Un design token es un valor con nombre que representa una decisión de diseño. En lugar de decir 'este botón es #0052CC', dices 'este botón usa el token de color brand-primary.' El token tiene un nombre, un valor y una categoría. El nombre es estable. El valor puede cambiar. Esta separación es lo que hace que los temas y el rebranding sean manejables.

Típicamente hay tres capas de tokens. Los tokens primitivos (también llamados globales o de referencia) son valores crudos: cada color en tu paleta, cada paso de espaciado, cada tamaño de fuente. Los tokens semánticos (también llamados alias o de decisión) referencian primitivos y llevan intención de diseño: color-button-primary-bg, spacing-section-gap, text-heading-large. Los tokens de componente son los más específicos y referencian tokens semánticos: button-primary-background, card-padding.

  • Primitivo: color-blue-600 = #0052CC
  • Semántico: color-action-primary = color-blue-600
  • Componente: button-primary-bg = color-action-primary

Este modelo de tres capas suena como overhead. No lo es. Es lo que te permite hacer un rebranding cambiando un solo mapeo de token semántico en lugar de buscar a través de cientos de componentes. Si cambia el color de acción primaria, la edición ocurre en un solo lugar, el mapeo semántico, y cada componente que consume color-action-primary lo recoge sin ser tocado.

Definiendo Tokens en Figma Variables

La funcionalidad de Variables de Figma (lanzada en 2023 y significativamente mejorada desde entonces) es ahora el lugar correcto para definir design tokens. Antes de Variables, los diseñadores usaban plugins como Tokens Studio. Variables es nativo, se sincroniza en tiempo real y tiene soporte directo para modos, que es cómo funcionan los temas en Figma.

Mi estructura en Figma usa colecciones organizadas por capa de token. Creo tres colecciones: Primitivos, Semánticos y Componente. Cada colección puede tener modos. La colección de Primitivos típicamente tiene solo un modo. Los valores no cambian entre temas. La colección Semántica tiene modos para Claro y Oscuro. La colección de Componente hereda de Semánticos.

  • Crea una colección 'Primitivos'. Todos los valores crudos de color, espaciado, radio, tipografía
  • Crea una colección 'Semánticos' con modos Claro y Oscuro. Los valores referencian Primitivos
  • Crea una colección 'Componente'. Los valores referencian tokens Semánticos
  • Nombra los tokens usando kebab-case para coincidir con las convenciones de CSS custom properties
  • Agrupa tokens usando notación de barra: color/action/primary. Figma renderiza esto como una jerarquía anidada
  • Documenta cada token semántico con una descripción: para qué es, para qué nunca debería usarse

La disciplina de nomenclatura es crítica. He visto sistemas donde los tokens se nombran por sus valores (blue-500, 16px) en lugar de por su propósito (action-primary, spacing-base). Cuando el valor cambia, el nombre se convierte en una mentira. Nombra los tokens por lo que significan, no por lo que actualmente son.

Exportando Tokens a CSS Custom Properties

El pipeline de exportación que uso va de Figma Variables a un archivo JSON de tokens a CSS custom properties. Las variables se exportan de Figma como JSON, el JSON se commitea al repositorio, y Style Dictionary hace la transformación. El paso de exportación importa menos de lo que la gente cree: cualquier plugin o script que escriba las variables como JSON sirve. Lo que importa es que el JSON aterrice en git y que nadie edite a mano el CSS generado.

El paso que la mayoría de los artículos se salta es cómo un modo de Figma se convierte en un archivo en disco. Una variable de la colección Semantic no tiene un valor, tiene un valor por modo, así que una sola exportación plana de esa colección es ambigua. Exporta una vez por modo y dale a cada exportación su propio directorio: tokens/primitives/ para la colección Primitives, tokens/semantic-light/ y tokens/semantic-dark/ para los dos modos de Semantic. Los nombres de los tokens son idénticos en los dos directorios de tema y solo cambian los valores. Eso es lo que permite que el build produzca un archivo CSS por tema.

Sobre la herramienta de exportación en sí, empieza gratis. Plugins de la comunidad como variables2json o Export Variables to JSON te dejan elegir una colección y un modo y guardar el resultado como JSON, que es todo lo que el pipeline necesita. La API REST de Variables de Figma es la forma más limpia de automatizar la misma exportación, pero está limitada al plan Enterprise. Muévete a ella solo si ya estás en Enterprise y quieres correr la exportación en CI en lugar de en el navegador de un diseñador.

Style Dictionary es una herramienta open-source de Amazon que transforma archivos de tokens a cualquier formato de plataforma: CSS custom properties, variables SCSS, constantes Swift de iOS, XML de Android. Para proyectos web, lo configuro para generar CSS custom properties organizadas por tema. Revisa tu versión mayor antes de copiar cualquier configuración. Todo lo de abajo es v3 y su API CommonJS. La v4 cambió el punto de entrada a una clase y eliminó token.attributes.category en favor de token.type, así que los filtros de v3 no se trasladan.

Figma Variables Exportación JSON tokens/*.json (git) Style Dictionary CSS Custom Props Angular Component
typescript
// build-tokens.js (Style Dictionary v3, CommonJS API)
const StyleDictionary = require('style-dictionary');

const cssPlatform = (destination, selector, outputReferences, filter) => ({
  css: {
    transformGroup: 'css',
    prefix: 'ds',
    buildPath: 'src/styles/tokens/',
    files: [
      { destination, format: 'css/variables', filter, options: { selector, outputReferences } }
    ]
  }
});

// One build per output file, each with its own source glob.
// Do not try to split a single build with filters on token.attributes:
// transformGroup 'css' runs attribute/cti, which overwrites attributes with
// category/type/item derived from the token path. A test like
// attributes.category === 'primitive' never matches and the file builds empty
// with no error.
const isSemantic = (token) => token.filePath.includes('/semantic-');

StyleDictionary.extend({
  source: ['tokens/primitives/**/*.json'],
  platforms: cssPlatform('primitives.css', ':root', false)
}).buildAllPlatforms();

// include loads the primitives so the var() references resolve.
// isSemantic keeps them out of the theme file.
StyleDictionary.extend({
  include: ['tokens/primitives/**/*.json'],
  source: ['tokens/semantic-light/**/*.json'],
  platforms: cssPlatform('semantic-light.css', ':root, [data-theme="light"]', true, isSemantic)
}).buildAllPlatforms();

StyleDictionary.extend({
  include: ['tokens/primitives/**/*.json'],
  source: ['tokens/semantic-dark/**/*.json'],
  platforms: cssPlatform('semantic-dark.css', '[data-theme="dark"]', true, isSemantic)
}).buildAllPlatforms();

La opción outputReferences: true es importante para los tokens semánticos. En lugar de resolver el valor hexadecimal final en tiempo de build, genera var(--ds-color-blue-600), lo que significa que el CSS retiene la cadena de referencias. Esto permite temas en runtime sin reconstruir los archivos de tokens.

El CSS Generado

Después de ejecutar Style Dictionary, obtienes archivos CSS que se ven así. Los primitivos definen valores crudos. Los tokens semánticos referencian primitivos. El cambio de tema se convierte en un atributo data en el elemento html.

typescript
/* primitives.css */
:root {
  --ds-color-blue-100: #EBF2FF;
  --ds-color-blue-200: #BDD5FF;
  --ds-color-blue-600: #0052CC;
  --ds-color-blue-700: #0043A6;
  --ds-color-neutral-0: #FFFFFF;
  --ds-color-neutral-900: #111827;
  --ds-spacing-1: 4px;
  --ds-spacing-2: 8px;
  --ds-spacing-4: 16px;
  --ds-spacing-6: 24px;
  --ds-spacing-8: 32px;
  --ds-radius-sm: 4px;
  --ds-radius-md: 8px;
  --ds-radius-lg: 16px;
}

/* semantic-light.css */
:root, [data-theme="light"] {
  --ds-color-action-primary: var(--ds-color-blue-600);
  --ds-color-action-primary-hover: var(--ds-color-blue-700);
  --ds-color-surface-default: var(--ds-color-neutral-0);
  --ds-color-text-primary: var(--ds-color-neutral-900);
  --ds-spacing-section: var(--ds-spacing-8);
  --ds-spacing-component: var(--ds-spacing-4);
}

/* semantic-dark.css */
[data-theme="dark"] {
  --ds-color-action-primary: var(--ds-color-blue-200);
  --ds-color-action-primary-hover: var(--ds-color-blue-100);
  --ds-color-surface-default: var(--ds-color-neutral-900);
  --ds-color-text-primary: var(--ds-color-neutral-0);
}

Consumiendo Tokens en Componentes Angular

Los archivos CSS se importan una vez en los estilos globales del proyecto Angular. Los componentes individuales nunca importan tokens directamente. Los consumen a través de la cascada. Este es el patrón correcto. Si un componente importa un archivo de tokens directamente, pierdes la capacidad de sobreescribir tokens mediante temas.

typescript
/* styles.scss, global entry point */
@use 'styles/tokens/primitives.css';
@use 'styles/tokens/semantic-light.css';
@use 'styles/tokens/semantic-dark.css';

/* Component styles, no direct token imports */
/* src/app/components/button/button.component.scss */
.btn-primary {
  background-color: var(--ds-color-action-primary);
  color: var(--ds-color-neutral-0);
  padding: var(--ds-spacing-2) var(--ds-spacing-4);
  border-radius: var(--ds-radius-md);
  border: none;
  cursor: pointer;
  transition: background-color 200ms ease;

  &:hover {
    background-color: var(--ds-color-action-primary-hover);
  }

  &:focus-visible {
    outline: 2px solid var(--ds-color-action-primary);
    outline-offset: 2px;
  }
}

Cambio de Tema en Runtime

Dado que los tokens semánticos referencian primitivos a través de cadenas var() que el navegador resuelve en tiempo de valor computado, el cambio de tema solo requiere establecer un atributo data-theme en el elemento document. Sin re-render, sin gimnasia de toggle de clases, sin mutaciones de estilo con JavaScript.

typescript
// theme.service.ts
import { Injectable, Renderer2, RendererFactory2 } from '@angular/core';

export type Theme = 'light' | 'dark';

@Injectable({ providedIn: 'root' })
export class ThemeService {
  private renderer: Renderer2;
  private currentTheme: Theme = 'light';

  constructor(rendererFactory: RendererFactory2) {
    this.renderer = rendererFactory.createRenderer(null, null);
    this.initTheme();
  }

  private initTheme(): void {
    const stored = localStorage.getItem('theme') as Theme;
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    const theme = stored ?? (prefersDark ? 'dark' : 'light');
    this.setTheme(theme);
  }

  setTheme(theme: Theme): void {
    this.currentTheme = theme;
    this.renderer.setAttribute(
      document.documentElement,
      'data-theme',
      theme
    );
    localStorage.setItem('theme', theme);
  }

  getTheme(): Theme {
    return this.currentTheme;
  }

  toggle(): void {
    this.setTheme(this.currentTheme === 'light' ? 'dark' : 'light');
  }
}

Sobreescrituras de Tokens a Nivel de Componente

Uno de los aspectos más poderosos de las CSS custom properties es que se propagan en cascada. Puedes sobreescribirlas en cualquier ámbito. Esto permite temas a nivel de componente sin duplicar estilos. Una tarjeta invertida, un banner promocional o una barra lateral con un esquema de color diferente puede sobreescribir solo los tokens que necesita.

typescript
/* Inverted surface, override tokens at component scope */
.surface-inverted {
  --ds-color-action-primary: var(--ds-color-blue-200);
  --ds-color-text-primary: var(--ds-color-neutral-0);
  --ds-color-surface-default: var(--ds-color-blue-700);

  background-color: var(--ds-color-surface-default);
  color: var(--ds-color-text-primary);

  /* All child components automatically use overridden tokens */
  /* No component code changes required */
}

Manteniendo Figma y el Código Sincronizados

El modo de fallo más común en sistemas de design tokens es la deriva. Figma se actualiza pero el código no, o viceversa. Yo abordo esto con un paso en el pipeline de CI que ejecuta Style Dictionary en cada merge a main y falla el build si la salida difiere de los archivos de tokens commiteados. Esto fuerza un commit deliberado cada vez que los valores de tokens cambian.

  • Exporta tokens desde Figma de forma programada (semanal o por release) y commitea el JSON fuente
  • Ejecuta Style Dictionary en CI. Falla el build si el CSS generado difiere del CSS commiteado
  • Usa versionado semántico para el paquete de tokens si se comparte entre múltiples apps
  • Documenta los cambios breaking en un CHANGELOG. Un renombrado de token es un cambio breaking para cada consumidor
  • Nunca dejes que desarrolladores individuales hardcodeen valores hexadecimales en CSS de componentes. Aplícalo mediante linting
  • Considera una regla de Stylelint que prohíba valores de color crudos y exija referencias var(--ds-)

El Retorno de la Inversión

La inversión en un sistema de tokens se recupera rápidamente. En mis últimos meses en Wonderlabs, en 2023, puse sobre este pipeline la base del design system que había construido para la PWA de tarjeta de crédito. No pudo haber sido antes: Figma recién lanzó Variables en junio de 2023. El flujo es el de este post: Figma Variables exportadas como JSON, Style Dictionary convirtiendo ese JSON en CSS custom properties, y componentes Angular consumiéndolas a través de la cascada. El beneficio es una propiedad de la estructura, no de un equipo en particular: los componentes construidos sobre tokens semánticos son tematizables por defecto, y probar un ajuste de color significa cambiar un token y volver a correr el build en lugar de editar estilos de componentes.

Los design tokens no se tratan de que Figma y CSS coincidan en un valor hexadecimal. Se tratan de crear un lenguaje compartido entre diseño e ingeniería que sobreviva a cambios de personal, rebranding y evolución de plataforma.

Empieza con los primitivos. Nómbralos por significado, no por valor. Construye tokens semánticos que capturen la intención de diseño. Exporta a CSS custom properties. Deja que la cascada haga el trabajo de temas. La misma estructura funciona para una sola app y para una librería de componentes compartida por varias apps de distintas marcas. Solo cambia el número de consumidores.

Referencias

Design SystemsCSSFigmaAngularDesign Tokens