---
id: design-system-from-code
domain: web-craft
agents: [ux-design-expert]
when: "ao consolidar um design system a partir de um codebase existente"
---

# Design system a partir de código — consolidar o que já existe, sem inventar

Codebase real não tem design system. Tem **arqueologia**: três anos de PRs, quatro pessoas com gostos
diferentes, um copy-paste de Figma que ninguém atualizou, e `#3B82F6` digitado à mão em 47 lugares. O
trabalho aqui **não é desenhar** — é *auditar, agrupar e migrar* o que está espalhado, transformando
caos em tokens reutilizáveis **sem quebrar quem consome**. A regra-mãe: **todo token tem que ter
origem rastreável no código atual.** Você não está criando uma paleta nova — está nomeando a que já
existe e matando as divergências. Inventar uma cor "mais bonita" no meio da consolidação é trair o
trabalho: vira redesign disfarçado de auditoria, e ninguém pediu redesign.

## O problema / os tells

Você está diante de um codebase que precisa de design system se vê 3+ destes:

1. **Hex hardcoded espalhado** — `grep -rE '#[0-9a-fA-F]{3,6}'` retorna centenas de hits. O mesmo azul
   aparece como `#3B82F6`, `#3b82f6`, `#3a83f7` (alguém errou de um dígito), `rgb(59,130,246)` e
   `rgba(59,130,246,1)`. São "o mesmo azul" pro olho, cinco valores distintos pro computador.
2. **Escalas tipográficas paralelas** — um componente usa `text-sm/text-base/text-lg` (Tailwind), outro
   tem `font-size: 15px` cravado, um terceiro herda `1.05rem` de um CSS legado. Três escalas, zero
   intenção. Você conta 11 tamanhos de fonte distintos numa tela só.
3. **Spacing ad-hoc** — `margin-top: 13px`, `padding: 18px 22px`, `gap: 6px`. Números que não pertencem
   a escala nenhuma, nascidos de "arrastar até parecer certo" no DevTools. O tell clássico: `17px`.
   Ninguém escolhe 17 de propósito.
4. **Variantes de componente duplicadas** — `Button`, `ButtonNew`, `PrimaryButton`, `CTAButton` e um
   `<button className="btn-primary">` solto. Fazem a mesma coisa, divergem em 3px de padding e um
   shade de hover. Cada um foi "mais rápido criar do novo do que achar o que existia".
5. **z-index sem sistema** — `z-index: 9999`, `z-index: 99999`, `z-index: 100`, `z-index: 9998` (o
   hack pra "ficar atrás do 9999"). Guerra de empilhamento, não arquitetura de camadas.
6. **Sombra e raio por gosto do dia** — `box-shadow` com 6 receitas diferentes de blur/spread/opacity;
   `border-radius` variando entre `4px`, `6px`, `8px`, `0.5rem` e `9999px` sem critério de uso.
7. **Dark mode meia-boca** — metade dos componentes responde a `.dark`, a outra metade tem branco
   cravado e "estoura" no tema escuro. Sintoma de cor hardcoded, não tokenizada.
8. **O Figma e o código discordam** — o token "primary" no design tool é `#2563EB`, no código rodando
   em produção é `#3B82F6`. A fonte da verdade é o que o usuário vê: **o código em produção**, não o
   arquivo de design. Comece pelo código.

Anti-tell (o que NÃO é problema): dois valores próximos podem ser **intencionais** — `border` em `1px`
e divisor em `1px` mas com cores diferentes é correto. Nem toda repetição é dívida; só a **divergência
sem razão** é. Não tokenize ruído como se fosse sinal.

## Os princípios do craft

### 1. Inventário antes de opinião — meça, não ache

Não comece propondo paleta. Comece **extraindo a verdade** do código com comandos, não com olho:

```bash
# Cores: todos os hex, normalizados e contados por frequência
grep -rohE '#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}|rgba?\([^)]+\)' src/ \
  | tr 'A-F' 'a-f' | sort | uniq -c | sort -rn

# Font-sizes: toda a escala real em uso
grep -rohE 'font-size:\s*[0-9.]+(px|rem|em)' src/ | sort | uniq -c | sort -rn

# Spacing: padding/margin/gap literais (o que escapou da escala)
grep -rohE '(margin|padding|gap)[^:]*:\s*[0-9.]+px' src/ | sort | uniq -c | sort -rn

# z-index: o mapa da guerra de empilhamento
grep -rohE 'z-index:\s*-?[0-9]+' src/ | sort | uniq -c | sort -rn

# Componentes que parecem o mesmo botão
grep -rlE 'Button|btn' src/components/ | sort
```

A saída de frequência é ouro: o hex que aparece **89 vezes** é seu primitivo; o que aparece **1 vez** e
difere por um dígito do primitivo é divergência pra eliminar. Frequência separa o sistema (alto uso,
intencional) do acidente (uso único, erro de digitação).

### 2. Cluster por proximidade perceptual, não por igualdade exata

`#3B82F6`, `#3b82f6` e `#3a83f7` são **o mesmo token**. Agrupe por distância de cor (ΔE), não por
string idêntica. Regra prática sem ferramenta: converta pra HSL e agrupe o que está dentro de ~2% em
cada canal. Cada cluster vira **um** primitivo; a variante mais frequente do cluster é a canônica, as
outras são aliases a migrar. Mesma lógica pra spacing: `13px`, `14px`, `15px` provavelmente queriam ser
`16px` — colapse pro valor da escala mais próximo, não preserve o acidente.

### 3. Primitivos → semânticos (duas camadas, nunca uma)

O erro fatal é tokenizar direto pra uso: `$button-blue: #3B82F6`. Quando a marca trocar de azul, você
caça `button-blue` em 200 lugares. Faça **duas camadas**:

```
PRIMITIVOS (a paleta crua, sem significado):
  --blue-500: #3B82F6
  --blue-600: #2563EB
  --gray-50:  #F9FAFB

SEMÂNTICOS (o significado, apontando pro primitivo):
  --color-action:        var(--blue-500)
  --color-action-hover:  var(--blue-600)
  --color-surface:       var(--gray-50)
  --color-text-primary:  var(--gray-900)
```

Componentes consomem **só semânticos** (`--color-action`), nunca primitivos diretos. Trocar a marca =
mudar um apontamento. Dark mode = remapear semânticos pra outros primitivos, sem tocar em componente.
Essa indireção é o que separa um design system de uma lista de cores com nome.

### 4. Nomeie por função, não por aparência

`--color-primary` é fraco (primário de quê?). `--color-danger`, `--color-action`, `--color-surface`,
`--color-border-subtle` carregam **intenção** — o consumidor sabe quando usar sem ler doc. E nunca
nomeie pela cor: `--color-green-success` quebra no dia que "sucesso" virar azul. O nome é o contrato;
a cor é detalhe de implementação.

### 5. Meça consistência com um número, priorize a dívida com ele

"Está inconsistente" não move ninguém. **Tokenization coverage** move: `% de valores que vêm de token
vs hardcoded`. Calcule por categoria:

```
cor:        38% tokenizado  → 62% hardcoded   (pior, ataca primeiro)
spacing:    71% tokenizado
tipografia: 84% tokenizado
```

Priorize por **frequência × visibilidade × esforço**: a cor de ação aparece em todo CTA (alta
visibilidade), tem 62% solto (alto volume) e é troca mecânica (baixo esforço) → primeira da fila. O
`z-index` aparece em 6 lugares e mexer arrisca regressão de overlay → depois, com cuidado. Não migre
em ordem alfabética; migre em ordem de retorno.

### 6. Migre com codemod e alias, nunca com find-and-replace cego

Trocar `#3B82F6` por `var(--color-action)` no braço pode quebrar onde aquele azul era **coincidência**
(um azul de ícone de terceiro que não devia seguir a marca). Migre assim:

1. **Camada de alias primeiro**: defina o token apontando pro valor atual exato. Nada muda visualmente
   — você só ganhou o nome.
2. **Codemod scoped**: substitua o literal pelo token **arquivo por arquivo**, rodando o diff visual
   (ou snapshot de teste) a cada lote. `jscodeshift`/`postcss` com escopo, não sed global.
3. **Deprecação, não deleção**: o componente velho (`ButtonNew`) ganha um `@deprecated` apontando pro
   canônico e continua funcionando. Você remove quando os consumidores migrarem — não no mesmo PR.
4. **Visual regression como rede**: snapshot antes/depois. Se um pixel mexeu e você não previu, a
   migração não era equivalente — investigue antes de seguir.

A migração correta é **invisível ao usuário**. Se a tela mudou, você fez redesign sem querer.

### 7. Component canônico = consolidar variantes na de maior cobertura, com props

Cinco botões viram **um** `Button` com variantes explícitas — `variant="primary|secondary|ghost"`,
`size="sm|md|lg"`. A base é a variante de **maior uso real** (a que mais aparece no grep), não a "mais
nova" nem a "mais bonita". As divergências de 3px que cada um tinha viram ou um token de tamanho, ou
são apagadas como o acidente que eram. Quem consumia `CTAButton` ganha um alias deprecado
`CTAButton = (p) => <Button variant="primary" {...p} />` — migra sem PR de breaking change.

### 8. A saída é três coisas, não um arquivo de cores

Entregue: **(a) tokens** em duas camadas, num formato consumível (CSS vars + JSON/W3C design tokens
pra ferramentas), **(b) componentes canônicos** com variantes e os antigos deprecados apontando pra
eles, e **(c) doc viva** que mostra cada token e componente *renderizado* (Storybook ou página de
estilo), com a regra de uso ("use `--color-action` em qualquer elemento clicável primário; nunca
hardcode azul"). Doc que é só uma tabela de hex morre na primeira semana; doc que renderiza o token e
diz quando usar é a que segura a consistência depois que você sai.

## Checklist

Antes de declarar o design system consolidado — qualquer "não" é trabalho pendente:

- [ ] Rodei o inventário por **comando** (grep de cor/fonte/spacing/z-index) e tenho a contagem de
      frequência, não só uma impressão?
- [ ] Agrupei valores por **proximidade perceptual** (clusters), não por string exata — e cada cluster
      tem um canônico definido pela frequência?
- [ ] Os tokens têm **duas camadas** (primitivo → semântico) e os componentes consomem só semânticos?
- [ ] Os semânticos são nomeados por **função** (`--color-action`), nunca por aparência
      (`--color-blue`)?
- [ ] Tenho um **número de cobertura** por categoria e a fila de migração está ordenada por retorno
      (frequência × visibilidade × esforço), não alfabética?
- [ ] A migração usa **alias + codemod scoped + deprecação**, com visual regression a cada lote — e
      nenhum consumidor quebrou?
- [ ] As variantes duplicadas viraram **um componente canônico** com props, e os antigos têm alias
      deprecado funcionando?
- [ ] A saída tem os **três artefatos** (tokens em formato consumível, componentes canônicos, doc viva
      que renderiza)?
- [ ] Cada token tem **origem rastreável** no código atual — não inventei cor/spacing "melhor" no meio?
- [ ] O usuário final **não vê diferença** depois da consolidação (migração invisível)?

## Antes → depois

| Tell (codebase sem sistema) | Craft (consolidação) |
|---|---|
| `#3B82F6` digitado em 47 lugares + 4 variantes por erro de dígito | Um cluster perceptual → `--blue-500` primitivo → `--color-action` semântico; aliases migrados por codemod |
| `--button-blue: #3B82F6` consumido direto nos componentes | Duas camadas: componente usa `--color-action`, que aponta pro primitivo; trocar marca = um apontamento |
| `margin-top: 13px`, `padding: 18px`, `gap: 6px` (números do DevTools) | Colapsados pra escala mais próxima (`16px`/`8px`); literais fora de escala viram exceção justificada ou somem |
| `Button`, `ButtonNew`, `PrimaryButton`, `CTAButton`, `.btn-primary` | Um `<Button variant size>` (base = a de maior uso); os 4 antigos são alias `@deprecated` que ainda funcionam |
| `z-index: 9999 / 99999 / 9998` (guerra de empilhamento) | Escala semântica de camadas: `--z-dropdown: 10`, `--z-modal: 40`, `--z-toast: 60` |
| "Está tudo inconsistente" (impressão) | "Cor: 38% tokenizado, 62% solto, 89 ocorrências do azul de ação → primeira da fila" (número) |
| Entrega: um `colors.ts` com 30 hex nomeados | Entrega: tokens 2 camadas (CSS+JSON) + componentes canônicos + doc que renderiza cada token e diz quando usar |
| Find-and-replace global de `#3B82F6` → quebra o azul de um ícone de terceiro | Alias primeiro, codemod scoped por arquivo, visual regression a cada lote, migração invisível |
