Voltar ao Blog
UX/UI Design17 de dezembro de 202511 min de leitura

Design Tokens com CSS Custom Properties: Do Figma ao Código

DR

Doug Ribeiro

11 min de leitura

Design tokens são a decisão estrutural de maior impacto em qualquer design system em que já trabalhei. Não são uma tendência nem um recurso do Figma. São a camada contratual entre design e código. Quando feito certo, uma mudança de cor no Figma se propaga para produção em minutos. Quando feito errado, você gasta semanas caçando valores hexadecimais hardcoded. Este post percorre todo o workflow que uso: de Figma Variables a CSS custom properties a componentes Angular, incluindo temas.

O Que Design Tokens Realmente São

Um design token é um valor nomeado que representa uma decisão de design. Em vez de dizer 'este botão é #0052CC', você diz 'este botão usa o token de cor brand-primary.' O token tem um nome, um valor e uma categoria. O nome é estável. O valor pode mudar. Essa separação é o que torna temas e rebranding viável.

Normalmente existem três camadas de tokens. Tokens primitivos (também chamados globais ou de referência) são valores brutos: cada cor na sua paleta, cada step de espaçamento, cada tamanho de fonte. Tokens semânticos (também chamados alias ou de decisão) referenciam primitivos e carregam intenção de design: color-button-primary-bg, spacing-section-gap, text-heading-large. Tokens de componente são os mais específicos e referenciam 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

Esse modelo de três camadas parece overhead. Não é. É o que permite fazer um rebranding mudando um mapeamento de token semântico em vez de caçar centenas de componentes. Se a cor de ação primária muda, a edição acontece em um lugar só, o mapeamento semântico, e todo componente que consome color-action-primary pega a mudança sem ser tocado.

Definindo Tokens em Figma Variables

O recurso Variables do Figma (lançado em 2023 e significativamente melhorado desde então) agora é o lugar certo para definir design tokens. Antes do Variables, designers usavam plugins como Tokens Studio. Variables é nativo, sincroniza em tempo real e tem suporte direto a modos, que é como temas funcionam no Figma.

Minha estrutura no Figma usa coleções organizadas por camada de token. Crio três coleções: Primitives, Semantic e Component. Cada coleção pode ter modos. A coleção Primitives tipicamente tem apenas um modo, pois os valores não mudam entre temas. A coleção Semantic tem modos para Light e Dark. A coleção Component herda de Semantic.

  • Crie uma coleção 'Primitives' com todos os valores brutos de cor, espaçamento, radius e tipografia
  • Crie uma coleção 'Semantic' com modos Light e Dark, com valores referenciando Primitives
  • Crie uma coleção 'Component' com valores referenciando tokens Semantic
  • Nomeie tokens usando kebab-case para corresponder às convenções de CSS custom properties
  • Agrupe tokens usando notação de barra: color/action/primary. O Figma renderiza isso como uma hierarquia aninhada
  • Documente cada token semântico com uma descrição: para que serve, para que nunca deve ser usado

A disciplina de nomenclatura é crítica. Já vi sistemas onde tokens eram nomeados por seus valores (blue-500, 16px) em vez de seu propósito (action-primary, spacing-base). Quando o valor muda, o nome vira mentira. Nomeie tokens pelo que significam, não pelo que são atualmente.

Exportando Tokens para CSS Custom Properties

O pipeline de exportação que uso vai de Figma Variables para um arquivo JSON de tokens e depois para CSS custom properties. As variáveis são exportadas do Figma como JSON, o JSON é commitado no repositório, e o Style Dictionary faz a transformação. O passo de exportação importa menos do que as pessoas pensam: qualquer plugin ou script que escreva as variáveis como JSON resolve. O que importa é que o JSON caia no git e que ninguém edite o CSS gerado na mão.

O passo que a maioria dos artigos pula é como um modo do Figma vira um arquivo em disco. Uma variável da coleção Semantic não tem um valor, tem um valor por modo, então uma única exportação plana dessa coleção é ambígua. Exporte uma vez por modo e dê a cada exportação seu próprio diretório: tokens/primitives/ para a coleção Primitives, tokens/semantic-light/ e tokens/semantic-dark/ para os dois modos de Semantic. Os nomes dos tokens são idênticos nos dois diretórios de tema e só os valores mudam. É isso que permite ao build produzir um arquivo CSS por tema.

Sobre a ferramenta de exportação em si, comece de graça. Plugins da comunidade como variables2json ou Export Variables to JSON deixam você escolher uma coleção e um modo e salvar o resultado como JSON, que é tudo de que o pipeline precisa. A API REST de Variables do Figma é o jeito mais limpo de automatizar a mesma exportação, mas está travada no plano Enterprise. Migre para ela só se você já estiver no Enterprise e quiser rodar a exportação no CI em vez de no navegador de um designer.

Style Dictionary é uma ferramenta open-source da Amazon que transforma arquivos de tokens em qualquer formato de plataforma: CSS custom properties, variáveis SCSS, constantes Swift para iOS, XML Android. Para projetos web, configuro para gerar CSS custom properties organizadas por tema. Confira sua versão major antes de copiar qualquer configuração. Tudo abaixo é v3 e a API CommonJS dela. A v4 mudou o ponto de entrada para uma classe e removeu token.attributes.category em favor de token.type, então os filtros da v3 não se aplicam.

Figma Variables Exportação 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();

A opção outputReferences: true é importante para tokens semânticos. Em vez de resolver o valor hexadecimal final no build, ela gera var(--ds-color-blue-600), ou seja, o CSS mantém a cadeia de referência. Isso permite temas em tempo de execução sem precisar reconstruir os arquivos de tokens.

O CSS Gerado

Após executar o Style Dictionary, você obtém arquivos CSS assim. Primitivos definem valores brutos. Tokens semânticos referenciam primitivos. A troca de tema se torna um atributo data no 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);
}

Consumindo Tokens em Componentes Angular

Os arquivos CSS são importados uma vez nos estilos globais do projeto Angular. Componentes individuais nunca importam tokens diretamente. Eles os consomem pela cascata. Esse é o padrão correto. Se um componente importa um arquivo de token diretamente, você perde a capacidade de sobrescrever tokens via 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;
  }
}

Troca de Tema em Tempo de Execução

Como tokens semânticos referenciam primitivos via cadeias var() que o navegador resolve no momento do valor computado, a troca de tema requer apenas definir um atributo data-theme no elemento document. Sem re-render, sem ginástica de toggle de classes, sem mutações de estilo via 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');
  }
}

Sobrescrita de Tokens no Nível do Componente

Um dos aspectos mais poderosos das CSS custom properties é que elas cascateiam. Você pode sobrescrevê-las em qualquer escopo. Isso habilita temas no nível do componente sem duplicar estilos. Um card invertido, um banner promocional ou uma sidebar com esquema de cores diferente podem sobrescrever apenas os tokens que precisam.

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 */
}

Mantendo Figma e Código Sincronizados

O modo de falha mais comum em sistemas de design tokens é a divergência: o Figma é atualizado mas o código não, ou vice-versa. Resolvo isso com um step no pipeline de CI que executa o Style Dictionary em cada merge para main e falha o build se o output divergir dos arquivos de token commitados. Isso força um commit deliberado sempre que os valores de token mudam.

  • Exporte tokens do Figma em base programada (semanal ou por release) e commite o JSON fonte
  • Execute o Style Dictionary no CI. Falhe o build se o CSS gerado divergir do CSS commitado
  • Use versionamento semântico para o pacote de tokens se compartilhado entre múltiplas apps
  • Documente breaking changes em um CHANGELOG. Uma renomeação de token é uma breaking change para todo consumidor
  • Nunca deixe desenvolvedores individuais hardcodar valores hexadecimais em CSS de componentes. Imponha via linting
  • Considere uma regra Stylelint que proíba valores de cor brutos e imponha referências var(--ds-)

O Retorno

O investimento em um sistema de tokens se paga rapidamente. Nos meus últimos meses na Wonderlabs, em 2023, coloquei nesse pipeline a base do design system que eu tinha construído para o PWA de cartão de crédito. Não daria para ser antes: o Figma só lançou Variables em junho de 2023. O fluxo é o deste post: Figma Variables exportadas como JSON, Style Dictionary transformando esse JSON em CSS custom properties, e componentes Angular consumindo tudo pela cascata. O ganho é uma propriedade da estrutura, não de um time específico: componentes construídos sobre tokens semânticos são temáveis por padrão, e testar um ajuste de cor significa mudar um token e rodar o build de novo em vez de editar estilos de componente.

Design tokens não são sobre Figma e CSS concordarem em um valor hexadecimal. São sobre criar uma linguagem compartilhada entre design e engenharia que sobrevive a trocas de pessoal, rebrandings e evolução de plataforma.

Comece com primitivos. Nomeie-os pelo significado, não pelo valor. Construa tokens semânticos que capturam a intenção do design. Exporte para CSS custom properties. Deixe a cascata fazer o trabalho de temas. A mesma estrutura funciona para um único app e para uma biblioteca de componentes compartilhada por vários apps de marcas diferentes. Só muda o número de consumidores.

Referências

Design SystemsCSSFigmaAngularDesign Tokens