Construtor de Sistema de Design
Isto não é uma lista genérica de melhores práticas. Cada regra abaixo existe porque ignorá-la produziu um bug real e enviado — a maioria deles silenciosos (sem erro, sem falha de build, apenas errado no navegador). references/bug-catalog.md tem o incidente específico por trás de cada uma. Leia-o uma vez antes de começar um sistema grande o suficiente para importar.
A linha condutora: uma regra que vive apenas em um comentário ou em um documento acabará sendo violada acidentalmente, e a violação não gerará erro. Cada fase abaixo termina perguntando "como isso é imposto automaticamente" — se a resposta for "alguém se lembra", a fase não está concluída.
Dois pontos de entrada:
- Construindo novo → Fases 1–7 em ordem.
- Auditando ou evoluindo um sistema existente → Fase 8, que aponta de volta para 1–7 conforme necessário.
Fase 1 — Descobrir
Pergunte antes de construir; adivinhar aqui produz um sistema que ninguém queria, e quando isso se torna visível, é caro desfazer.
- Material de referência como exemplos literais, não como adjetivos. "Moderno e limpo" não especifica nada; três capturas de tela sim.
- A cor carrega significado, ou é apenas estética? Se uma paleta tem valores categóricos (elemento, status, linha de marca), decida de antemão se cada um é também um comportamento (movimento, forma, peso) ou puramente visual. Retrofitar significado a uma cor escolhida pela aparência é muito mais difícil do que decidir isso primeiro.
- Sobre o que o conteúdo se apoia? Não presuma que o fundo de uma página e o fundo de um painel são "quase iguais". Meça combinações candidatas antes de se comprometer — veja a Fase 2.3.
- Escopo: nova construção, refinar um sistema existente ou comparar com um sistema anterior/irmão? Rotas para a Fase 8 se não for uma construção nova e limpa.
- Distribuição: um projeto ou reutilizado em muitos? Esta única resposta decide se a Fase 6 (empacotamento) se aplica. Erre aqui e você superengenha algo de uso único ou subengenha algo que está prestes a se espalhar por três repositórios.
Fase 2 — Autorar tokens
Em ordem de prioridade — erre estes e tudo construído sobre eles herda a fundação errada.
2.1 — Cores são autoradas uma vez, como tripletos de canal brutos, nunca como hex livre.
--fire-rgb: 170 0 0; /* o único valor autorado */
--fire: rgb(var(--fire-rgb)); /* derivado */
--fire-soft: rgb(var(--fire-rgb) / 0.18); /* derivado */
Isso não é estilístico. Os modificadores de opacidade do Tailwind (e da maioria dos sistemas utilitários) (bg-fire/40) não podem compor um canal alfa em um valor hex opaco var(--x) — a classe resolve silenciosamente para nada, sem erro, sem aviso. Este bug exato é o que iniciou a passagem de fortalecimento da qual esse padrão vem. Um número autorado por cor também significa que há apenas um lugar para alterá-lo.
2.2 — Toda cor que pode ser renderizada como texto ganha uma variante -text, e os glifos a usam, nunca a base.
Verifique o contraste calculando a luminância relativa WCAG (fórmula em assets/check-tokens.template.mjs), não a olho nu. Um matiz que atinge 4,5:1 contra uma superfície pode falhar contra outra — que é exatamente por que 2.3 importa.
2.3 — Se houver mais de um nível de superfície (página / painel / rebaixado), defina-o como uma escala de elevação real e meça a separação entre os níveis antes de se comprometer. "Um pouco mais claro" não é uma decisão, é um palpite. Um sistema real inverteu um modelo de elevação inteiro (tinta-como-página → vazio-como-página) depois de enviá-lo, puramente porque alguém finalmente calculou o contraste composto real: 1,04:1 de um lado, 1,19:1 do outro. Faça essa aritmética antes de construir sobre a escolha, não depois que uma parte interessada disser "os cartões parecem planos".
Uma vez que houver uma escala, todo token seguro para texto precisa ter o contraste verificado contra cada nível em que pode aparecer, não apenas aquele para o qual foi projetado.
2.4 — Movimento: uma curva de easing, um pequeno conjunto fixo de durações.
Se alguma animação JS (framer-motion, GSAP, um requestAnimationFrame manual) consumir esses valores, exporte a curva e as durações como constantes JS junto com as CSS — uma variável CSS é invisível para JavaScript, e uma bezier ou duração digitada manualmente se desvia imediata e invisivelmente. Mantenha as versões CSS e JS verificadas entre si (Fase 4).
2.5 — Limites numéricos em qualquer coisa semelhante a brilho/gradiente devem ser números reais, verificados contra o valor realmente enviado — incluindo paradas de gradiente, não apenas cores simples de alfa único. "Brilho sutil" sem um teto imposto é como o brilho ambiente de página inteira retorna, um componente de cada vez, cada um individualmente com aparência razoável.
2.6 — Cada token recebe um comentário de uma linha dizendo quando usá-lo, não apenas como ele é renderizado.
Fase 3 — Construir primitivas
- Componentes referenciam tokens exclusivamente. Sem hex, sem valores arbitrários, sem literais do tipo
white/10— estes são exatamente o que a guarda da Fase 4 verifica, então escrevê-los agora significa apenas falhar na verificação depois. - Qualquer uso de modificador de opacidade (
bg-x/10) deve ter como alvo um token que seja realmente composível em alfa conforme 2.1 — verificável, então verifique, não avalie a olho. - Não codifique uma variante nas classes base de um componente se uma prop deve substituí-la.
className="shadow-lg"embutido em cada cartão, com uma prop que deve substituir porshadow-root-earth, produz dois utilitários de box-shadow com especificidade igual — o vencedor é decidido pela ordem de origem na folha de estilo gerada, silenciosamente. Torne a variante uma tabela de consulta chaveada pela prop (Record<Variant, string>), nunca uma classe base mais uma substituição esperada. - Uma prop de estilo não substitui um elemento interativo real. Um cartão que parece clicável (
cursor-pointer, elevação ao passar o mouse) via uma propinteractive, mas é uma<div>, dá a um usuário de mouse uma affordance que um usuário de teclado nunca recebe — sem anel de foco, sem role, sem ativação. Se algo deve ser clicado, renderize um<a>ou<button>(um parCardLink/CardButton), não uma alternância de estilo na versão passiva. - Para componentes interativos genuinamente complexos (modal, abas, toast), aproveite a plataforma. O
<dialog>nativo fornece uma armadilha de foco e Escape-para-fechar gratuitamente; versões feitas à mão invariavelmente erram um desses. O roving tabindex para abas é a coisa mais comum que implementações de abas feitas à mão erram (cada aba inativa permanece um ponto de parada de Tab) — verifique explicitamente (os tabindexes se movem[0,-1,-1,-1] → [-1,0,-1,-1]na navegação por seta), não presuma. - Se o sistema precisar de ativos de ícone/marca personalizados, fixe as regras de desenho antecipadamente (grade, peso do traço, estilo de ponta) e imponha-as da mesma forma que as regras de cor — isso também é trabalho da Fase 4.
Fase 4 — Proteger
Escreva um script de verificação pequeno, idealmente com zero dependências. Este é o artefato de maior alavancagem em todo o processo — cada bug em references/bug-catalog.md era uma regra que existia apenas em prosa até que alguém a codificou aqui.
Cobertura mínima (adapte assets/check-tokens.template.mjs):
- Cada token que um componente ou predefinição referencia realmente existe.
- Nenhum hex digitado manualmente ou cor de opacidade literal no código-fonte do componente.
- Contraste WCAG para cada token seguro para texto contra cada nível de superfície em que pode aparecer (2.3).
- Limites numéricos documentados impostos como números no valor real enviado — incluindo a parada mais brilhante de um gradiente, não apenas cores simples (2.5).
- Constantes de movimento JS correspondem às suas contrapartes de token CSS (2.4).
- Regras estruturais que são fáceis de violar acidentalmente — por exemplo, "sem brilho de fundo de página inteira": verifique se um utilitário de brilho nunca é aplicado a um elemento de nível de página, não apenas que o alfa do token é limitado.
- Escopo qualquer regra que possa corresponder a prosa comum (por exemplo, uma regra sobre "uma segunda curva de easing") para contextos de código reais — atributos de classe, valores CSS — não texto arbitrário. Um linter que sinaliza sua própria documentação acaba sendo desabilitado, o que anula o propósito.
A verificação deve eventualmente ser executada contra projetos consumidores, não apenas o próprio repositório do sistema. Esta é a maior lição disponível aqui: em um sistema real, toda violação de regra que realmente aconteceu — onze tamanhos de fonte abaixo de 24px, três tamanhos diferentes de <h2> em um site, cinco valores de letter-spacing concorrentes, uma segunda curva de easing, durações inventadas — aconteceu em um projeto consumindo o sistema, e a verificação interna nunca olhou lá. Por muito tempo, estava protegendo código que nunca havia quebrado uma regra. Envie o verificador para que seja executável contra código-fonte arbitrário (npx your-check ./src) e dê a ele uma escotilha de escape com escopo para exceções genuínas (uma convenção de comentário your-check-allow: <rule> — <reason>), em vez de fazer as pessoas desabilitarem a verificação inteira.
Teste negativamente cada verificação antes de confiar nela. Quebre deliberadamente cada regra em uma cópia de rascunho e confirme que o script realmente falha nela. Uma verificação que nunca foi observada falhar não é verificada — você não sabe se funciona ou está silenciosamente inerte.
Fase 5 — Documentar
Três arquivos, divididos por público-alvo — resista ao impulso de adicionar um quarto. Um quarto documento (um "design.md" agregado, um segundo guia de estilo como documento) é um segundo lugar para os mesmos valores se desviarem; três que cada um possui uma questão distinta (porquê / o-que-fazer / o-que-foi-decidido) é suficiente.
philosophy.md— o porquê, a identidade, o significado por trás das escolhas. Lido uma vez, raramente relido.foundations.md— as regras operacionais. O que alguém verifica antes de construir algo novo. Deve mapear aproximadamente 1:1 para o que a guarda da Fase 4 impõe — se uma regra está em um e não no outro, escolha qual está faltando e corrija.decisions.md— um registro contínuo, entrada mais recente primeiro, de julgamentos específicos e porquê. Nunca exclua uma entrada substituída. Marque-a como substituída e explique o que mudou e porquê. Reverter uma decisão real (um modelo de elevação, uma hierarquia de acento) sem uma trilha de papel força a próxima pessoa — muitas vezes o você do futuro — a derivar o raciocínio do zero, ou pior, a quebrar silenciosamente o que a decisão original corrigiu.- Uma vitrine viva que renderiza os tokens e componentes reais — não uma descrição deles. Se não puder literalmente
importaro pacote (um documento HTML estático de arquivo único, por exemplo), está carregando uma segunda cópia inline de cada valor, e essa cópia vai se desviar: a vitrine de um sistema real ficou silenciosamente duas versões atrás de seus próprios tokens antes que alguém a reconstruísse como algo que importa o pacote real. A correção mais rápida, se uma vitrine estática for inevitável por um tempo, é a Fase 4 protegendo a cópia inline contra a fonte real — mas planeje aposentá-la assim que o sistema estiver estável o suficiente para ter um consumidor real. - Se a vitrine for implantável, implante-a em uma URL real e visualize-a em uma viewport real e estreita. Um bug de layout que só aparece quando uma seção específica é mais estreita que seu conteúdo (um elemento decorativo intencionalmente transbordando alargando todo o documento, por exemplo) é invisível para todas as verificações estáticas — não é uma violação de token, não é uma falha de contraste, não é um erro de lint. Ele só existe em um navegador real em uma largura real. Um desses bugs foi enviado e sobreviveu a três versões porque o único lugar onde o sistema era visto era um arquivo local.
- Escreva uma política de versionamento e a ordem para ler os documentos, uma vez que houver mais de um documento. Custa um parágrafo, salva alguém de derivá-lo novamente.
Fase 6 — Empacotar e distribuir
Somente se a Fase 1 disse "reutilizado em projetos".
- Envie como um pacote instalável (
package.jsoncom um mapaexports). Nunca "copie estes arquivos para seu projeto" — copiar não tem caminho para uma correção downstream fluir de volta upstream, que é como a própria fonte da verdade de um sistema silenciosamente se torna a mais quebrada de duas cópias. Esta foi a causa raiz do primeiro bug em toda esta linhagem. - Se estiver distribuindo uma predefinição do Tailwind: o array
contentde uma predefinição NÃO é mesclado pelo Tailwind. O próprio arraycontentde um consumidor substitui completamente o da predefinição. Este é o segundo bug mais caro no catálogo, e a correção "óbvia" — declarar os globs na predefinição e presumir que eles se combinam — não funciona; o build volta byte-idêntico, que é o indicador. A predefinição deve exportar seus globs, e cada peça de integração (README, um comando de scaffold se existir) deve mostrar os consumidores espalhando-os explicitamente:content: [...preset.content, ...yourGlobs]. Verifique isso com a própria API de resolução de configuração do bundler, não presuma — um artefato de build inalterado após uma "correção" é um sinal de bug, não uma coincidência. - Versione com tags, não apenas branches. Um pin de branch se move silenciosamente sob um consumidor no momento em que o branch do repositório de origem avança — que é exatamente como um consumidor pode acabar enviando uma versão não testada sem que ninguém decida isso. Se um consumidor precisar mover para uma nova tag, lembre-se de que lockfiles podem fixar o commit resolvido exato independentemente do que a tag em
package.jsondiz — force a re-resolução e verifique senode_modulesrealmente relata a nova versão antes de confiar em um rebuild. - Se o pacote oferecer um comando de scaffold/init para novos consumidores, faça com que ele emita a configuração de conteúdo correta e espalhada desde o primeiro dia — não confie na disciplina de leitura de README de cada consumidor para uma armadilha séria o suficiente para ter sua própria entrada no registro de decisões.
Fase 7 — Verificar
Aplica-se continuamente, não apenas uma vez no final.
- Teste contra uma instalação ou clone limpos, não o diretório de trabalho. Um diretório de trabalho esconde exportações ausentes, correções não commitadas e caches obsoletos que um consumidor real encontrará no primeiro dia.
- Um artefato de build inalterado após uma "correção" é um sinal de que a correção não funcionou, não uma coincidência. Compare a saída real antes de acreditar que algo mudou.
- Typechecking não é verificação. O bug mais sério neste catálogo — uma fonte renderizando no tipo de letra completamente errado — passou em todas as verificações estáticas disponíveis e só foi pego ao renderizar o componente em um consumidor real e olhar para ele. Renderize tudo pelo menos uma vez em algo real antes de considerar concluído.
- Desconfie de APIs de plataforma de aparência conveniente para alegações de verificação.
document.fonts.check()parece exatamente a ferramenta certa para "essa fonte realmente contém este glifo" e mente (relata disponibilidade mesmo quando a fonte está ausente em alguns navegadores). Teste o comportamento real, não uma API que promete verificar.
Fase 8 — Auditar ou evoluir
Para sistemas existentes. Aplique isto como uma passagem de diagnóstico antes de tocar em qualquer token ou componente.
- Execute a verificação da Fase 4 contra o sistema existente, mesmo que ele nunca tenha tido uma. Os resultados lhe dirão: (a) quais regras estão sendo violadas agora e (b) se alguma verificação que você esperava falhar não falhou — o que significa que pode estar faltando uma verificação.
- Diferencie entre violações quebráveis e violações acidentais.
- Uma violação quebrável: um token cujo valor está errado e que você pode corrigir sem efeito colateral. Corrija diretamente.
- Uma violação acidental: uma regra que o sistema nunca pretendeu seguir (por exemplo, "toda cor tem um canal alfa" quando seu sistema foi construído antes dessa regra existir). Decida se a regra deve ser aplicada retroativamente ou se o sistema existente obtém uma escotilha de escape com escopo (veja Fase 4) até uma migração planejada.
- Olhe para
decisions.mdse ele existir. Se alguma decisão importante não estiver documentada, adicione-a com uma nota sobre como você a descobriu. - Verifique a consistência da vitrine contra o pacote enviado — veja a Fase 5, nota sobre desvio de vitrine. Se a vitrine é uma cópia estática, marque-a como obsoleta e priorize movê-la para importações reais.
- Teste a distribuição: se o sistema já está sendo consumido por vários projetos, verifique se cada consumidor tem a versão mais recente, ou pelo menos tenha um diff claro entre o que eles têm e o que o sistema declara. Isso é frequentemente a fonte mais dolorosa do "funciona na minha máquina", onde o sistema parece correto no repositório de origem, mas as importações reais estão desatualizadas.
- Produza um relatório listando:
- O que foi corrigido imediatamente.
- O que precisa de uma mudança quebrando e em que cronograma.
- Quais verificações foram adicionadas ou faltando.
- Uma estimativa de quanto do sistema existente está em conformidade com as regras atuais (porcentagem de tokens aprovados, componentes aprovados, etc.).