DEV Community

Guilherme Marucchi
Guilherme Marucchi

Posted on

# Instalar uma biblioteca de CSS quase apagou um botão do sistema

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;
`
Enter fullscreen mode Exit fullscreen mode

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)}>
Enter fullscreen mode Exit fullscreen mode

Um botão real, numa tela real, com uma classe chamada collapse porque ele colapsa um painel.

O Tailwind tinha gerado:

.collapse { visibility: collapse }
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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) { ... }
Enter fullscreen mode Exit fullscreen mode

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 *));
Enter fullscreen mode Exit fullscreen mode

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)