Eu tinha acabado de instalar o Tailwind em um sistema que roda sobre styled-components há anos. Build passou. Cinco mil e quinhentos testes, verdes. Novecentos e sessenta snapshots, intactos.
Podia ter seguido em frente. Mas abri o CSS que o build tinha gerado só para ver como estava.
Havia 41 classes utilitárias ali dentro. E eu não tinha escrito nenhuma classe do Tailwind no projeto.
O que estava acontecendo
O Tailwind não sabe quais classes você vai usar. Ele descobre lendo os seus arquivos e procurando algo que
pareça nome de classe. É por isso que ele é rápido: em vez de gerar milhares de regras e você jogar fora o
que não usa, ele gera só o que encontrou.
O detalhe é que essa leitura é textual. Ele não entende TypeScript, não sabe o que é JSX, não distingue
uma string de um comentário. Ele varre o arquivo procurando padrões que se pareçam com classes.
E num projeto com styled-components, os arquivos de estilo são cheios de texto assim:
export const Wrapper = styled.div`
display: flex;
position: absolute;
visibility: collapse;
`
Para o Tailwind, aquele flex é um candidato. absolute também. E collapse também.
Eram esses os 41.
O problema não era o desperdício
Oito quilobytes de CSS que ninguém usa é chato, não é grave. O problema apareceu quando fui cruzar as classes
geradas com as que o sistema já usava de verdade.
Doze colidiam. E uma delas era esta:
<button className="collapse" onClick={() => setIsOpen(!isOpen)}>
Um botão real, numa tela real, com uma classe chamada collapse porque ele colapsa um painel.
O Tailwind tinha gerado:
.collapse { visibility: collapse }
Em elemento que não é tabela, visibility: collapse se comporta como hidden.
Aquele botão desapareceria da tela.
"Mas o CSS antigo não venceria?"
Foi a primeira coisa que eu pensei. As duas regras miram o mesmo elemento, e o styled-components injeta o CSS
dele fora das cascade layers. O que, pela especificação, sempre ganha de quem está dentro de uma layer, como
o Tailwind está.
Só que a cascata não escolhe um vencedor por regra. Ela escolhe por propriedade.
A regra do sistema era assim:
button.collapse {
appearance: none;
border: none;
outline: none;
cursor: pointer;
background-color: transparent;
}
Cinco propriedades. Nenhuma delas é visibility.
Sem disputa, não há vencedor: as duas regras simplesmente se somam. O visibility: collapse do Tailwind
entrava sem encontrar resistência.
Essa é a parte que eu tinha entendido errado, e é a que mais vale levar daqui: layer e especificidade só
resolvem conflito na mesma propriedade. Onde uma regra declara algo que a outra não declara, as duas
valem.
Por que ninguém teria percebido
É aqui que a coisa fica desconfortável.
- O build passa. É CSS válido
- Os testes passam. Eles verificam comportamento e marcação, não estilo computado
- Os snapshots passam. A marcação não muda — o botão continua no DOM, com a mesma classe
- A revisão de código não pega. O diff mostra um arquivo de configuração de três linhas
Não existe ferramenta no caminho normal que veja isso. O único jeito de encontrar era fazer o que eu fiz por
acaso: abrir o CSS gerado e olhar.
E o mesmo mecanismo tinha produzido .container { width: 100% }, que se aplicaria a 22 elementos espalhados
por dez arquivos.
A correção, e o que ela revelou
O Tailwind aceita desligar a varredura automática e declarar explicitamente onde ele pode procurar:
@import 'tailwindcss/utilities.css' layer(utilities) source(none);
@source '../components';
Segundo build: zero classes geradas a partir do código antigo. Zero colisões.
Mas o que me fez gostar dessa correção não foi ela resolver o bug. Foi o efeito colateral.
O projeto está numa migração gradual: um design system novo entrando enquanto o antigo sai, tela por tela. A
regra número um dessa migração é que os dois não se misturem no mesmo arquivo — os dois geram classes com a
mesma especificidade, e quem vence passa a depender de detalhes que ninguém controla.
Essa regra estava num documento. Documento que alguém precisa ler, lembrar e aplicar.
Com o @source restrito, ela deixou de precisar disso: classe do padrão novo escrita dentro de um arquivo
do padrão antigo simplesmente não é gerada. O componente aparece sem estilo, a pessoa percebe na hora, e
ninguém precisa ter lido nada.
A regra saiu do documento e virou comportamento de build.
Não foi sorte
Sei disso porque a mesma prática pegou um segundo problema, no mesmo dia.
Os componentes que vêm do catálogo trazem classes de tema escuro de fábrica — coisas como
dark:bg-destructive/60. Vieram dez delas junto com os dois primeiros componentes.
No comportamento padrão da ferramenta, o prefixo dark: compila para:
@media (prefers-color-scheme: dark) { ... }
Ou seja: essas dez regras passariam a valer sozinhas para todo usuário com o sistema operacional em tema
escuro. Num sistema que não tem tema escuro nenhum.
Uma parcela enorme de gente usa o sistema em modo escuro. Todos eles veriam botões com tons ligeiramente
diferentes, bordas trocadas e hovers estranhos.
A correção é uma linha, que prende o prefixo a uma classe em vez de ao sistema:
@custom-variant dark (&:is(.dark *));
Como essa classe não existe no projeto, as dez regras ficam inertes. E no dia em que o tema escuro entrar,
todas passam a valer de uma vez.
O que eu levo disso
Build verde não é verificação, é ausência de erro de compilação. São coisas diferentes, e a distância
entre elas é exatamente onde mora a classe de bug que ninguém encontra.
Ler a saída da ferramenta é barato. Os dois problemas apareceram em minutos, porque eu olhei o CSS
gerado em vez de confiar no build. O custo de olhar é sempre menor que o custo de descobrir depois — e
"depois" aqui significaria um chamado de usuário dizendo que não consegue mais abrir uma tela.
A melhor correção é a que transforma a regra em comportamento. Toda migração produz um monte de "não faça
X". Cada uma dessas regras depende de alguém lembrar, e memória de time não escala. Quando dá para fazer a
ferramenta recusar, a regra para de ser combinada e passa a ser verdade.
Este é o primeiro de três textos sobre migrar um design system num sistema em produção sem parar a entrega.
O próximo é sobre conviver com duas identidades visuais ao mesmo tempo, e como uma camada de tokens
transformou uma troca de marca em quatro linhas alteradas.
Top comments (0)