Criei Minha Própria Lib CSS-in-JS! Por Quê? (Parte 1)
Opa!
Um tempinho atrás me dediquei a um projeto chamado Kamishibai, um web app através do qual jogadores de RPG poderiam fazer o controle das quests realizadas pelo grupo. Escrevi a codebase usando a T3 Stack, que usa React com TypeScript, NextJS e tRPC. Até o momento da escrita desse texto, essa aplicação não chegou à versão 1.0, então não gostaria de falar muito sobre ele agora; em vez disso, quero falar sobre uma distração que foi tomando proporções cada vez maiores em relação à estilização dele.
Esse é o primeiro de dois textos que decidi escrever sobre a experiência que tive de criar minha própria biblioteca de estilização, e neste, gostaria de seguir o conselho do Simon Sinek e explicar primeiramente: por que diabos eu faria isso?
Introdução
Hoje em dia temos tantas opções, pra todos os gostos; temos o Tailwind, queridinho de alguns e odiado de outros, temos libs antigas mas que ainda são mantidas e bastante usadas como o styled-components e vanilla-extract, temos opções novas e chamativas como o StyleX e o Panda. E isso só falando sobre escolhas para criar seus estilos do zero, se entrarmos na discussão de bibliotecas de componentes, já entra o veterano MUI, o hypado (com razão) shadcn/ui, o infelizmente ainda não esquecido Bootstrap… enfim, várias opções ótimas e ruins também. Isso tudo, é claro, na presunção que eu não decidisse simplesmente usar o bom e velho CSS puro.
Para evitar a paralisia por excesso de opções, reduzi as minhas escolhas.
À época que comecei a trabalhar no projeto, o Tailwind havia se tornado a opção padrão de facto de estilização no ecossistema NextJS. Há alguns motivos históricos não tão simples, diferentes e cumulativos para isso ter acontecido:
- A partir da versão 13 do NextJS, o framework passou implementar os React Server Components. Essa mudança quebrou a forma como a maioria das bibliotecas CSS-in-JS funcionava à época;
Simplificando bastante, as bibliotecas tendiam a gerar os estilos no lado do cliente, por ser necessário rodar código JavaScript para tal (é, afinal, CSS-in-JS). Como agora o React por padrão rodava o código para gerar as páginas apenas no servidor, em resumo, tudo quebrava.Exceto que, sabe o que não quebrava? O Tailwind, que por padrão já tinha um passo de pré-compilação (afinal, todas as classes já estão declaradas).
- O Tailwind já vinha ganhando popularidade no ecossistema JavaScript por ser uma maneira agnóstica de framework de se estilizar aplicações.
- A agilidade ganha ao se utilizar Tailwind, principalmente no caso de uso de MVPs que historicamente são um caso de uso comum para o Next fez a lib ganhar notoriedade o bastante para ser a opção de estilização padrão recomendada pela Vercel no
create-next-app
(em desfavor ao CSS Modules que era o padrão anterior).
Apesar de eu não estar utilizando o App Router nesse projeto específico e por consequência não estar preocupado com os Server Components, decidi que seria de bom tom utilizá-lo tanto para facilitar uma possível migração futura quanto por outra razão mais prática: eu já estava acostumado com o Tailwind por usá-lo no trabalho.
Cheguei a analisar outras opções de CSS-in-JS. Afinal, havia uma outra ferramenta que eu estava acostumado por ter uma DX sensacional (que até prefiro à do Tailwind): o finado Stitches. Stitches é uma lib que chegou a ganhar uma boa popularidade entre desenvolvedores que trabalham com NextJS pois tinha uma ótima performance com o Server-Side Rendering; entretanto acabou sendo descontinuado à época do lançamento do Next 13, então nunca foi adaptado para funcionar com React Server Components.
É uma pena que não era possível ter a DX do Stitches nesse projeto, afinal fazia bem mais sentido utilizar o Tailwind. Bem, talvez não seria necessário escolher… 🤔 Mas estou me adiantando.
Primeiramente, acho de bom tom entender mais sobre como funciona cada um deles.
Stitches
Além de lembrar uma música de sucesso do Shawn Mendes (e, no meu caso, uma de menos sucesso do Foo Fighters), o Stitches se destacava entre as outras dezenas de bibliotecas CSS-in-JS da época por conta de alguns fatores que resultavam em uma experiência de desenvolvimento ótima com uma performance admirável — uma distinção e tanto, uma vez que essas bibliotecas já foram infames por pesar bundles.
O primeiro fator era a compilação dos estilos com antecedência. Através do Stitches, o CSS gerado não exigia JavaScript rodado no lado do navegador, o que combina bem com um framework que procura fazer o máximo de trabalho possível no servidor.
O segundo é de ser uma biblioteca orientada a design system. Isso exige um investimento inicial de tempo na configuração, mas facilita muito a manutenção, criação e composição de novos estilos.
Isso tudo significa que era muito fácil criar, digamos, um botão base…
… E a partir dele, criar um botão de confirmar e outro de cancelar.
Apesar de todos esses benefícios, ainda não falei do que eu mais gostava de usar no Stitches, e, de fato, separei a próxima seção só para isso.
Pattern styles.ts
O motivo do nome CSS-in-JS é que a forma mais comum de usá-lo é (ou já foi) exatamente da primeira forma que a gente imaginaria ao ouvir esse termo: estilos CSS sendo declarados em conjunto com o resto do nosso código JavaScript.
De fato, o JavaScript é essencial pois é o que permite as interações quase mágicas que essas libs permitem. Quer colocar largura máxima no botão só quando o nome do usuário for “Teste123”? Claro, disponha!
… Mas outra característica da nossa linguagem da web é a de ser modular. Isso significa que nós também podemos declarar os nossos estilos em um arquivo JavaScript separado…
… E depois simplesmente importá-lo onde precisar.
Eu particularmente gosto muito desse pattern. Consigo manter as coisas separadas, o código limpo e não me incomodo de ter que trocar de contexto nesses casos. É claro, o tradeoff é que tem algumas coisas que ficam difíceis de serem usadas se não está tudo no mesmo arquivo; por exemplo, se houver alguma constante em algum dos arquivos que ambos precisem acessar…
… Esta precisará ser exportada.
Contras
Nem tudo são rosas em relação ao uso do Stitches, mesmo descontando a impraticidade de atualizar o projeto para usar React Server Components e o fato de ter sido descontinuado.
Bundle de crescimento linear
A performance do Stitches é certamente melhor que a de seus concorrentes na época, mas, criticamente, o peso dele no bundle CSS escala em relação quase linear à quantidade de estilos escritos através dele. No começo não faz muita diferença, claro, mas isso muda conforme o projeto cresce.
Complicações na organização do código
Como mencionei na seção anterior, é preciso escolher qual preço pagar: exportar seus estilos do arquivo styles.ts
, lidar com mudanças de contexto e ter acesso dificultado ao uso de código dependente de JavaScript ou manter tudo em um mesmo grande arquivo poluído?
Tailwind
Como deixei claro acima, acabei optando pela opção de provável maior hype, decidindo utilizar o Tailwind. Mas essa ferramenta é mais do que apenas isso, então vamos lá, acho que é justo elencar aqui por que alguém poderia querer ou não utilizá-lo.
Menos trocas de contexto
Um ponto de venda recorrente do Tailwind. Usar as utility classes dele significa que não é necessário, em 99% do tempo, pensar em nomes de classes, criar novas classes, ou visitar arquivos diferentes para criar, estender ou checar a estilização de um componente. Toda a informação de estilo de um dado componente está visível ali.
Existe um ganho de produtividade real quando essa troca de contexto é eliminada (após o breve período onde ela é apenas substituída pela troca de contexto de olhar a documentação do Tailwind), o que o fez ser visto de maneira favorável por muitos desenvolvedores.
Ecossistema forte
Há diversas vantagens em usar uma ferramenta popular. Uma delas é saber que outras pessoas mais espertas que você também usam, e podem já ter resolvido problemas que você está tendo. Esse é o caso do Tailwind.
Onde mais tem uma ferramenta de estilização com uma comunidade forte de plugins, que vão desde animações, até formulários? A equipe do Tailwind pode demorar para se adequar às novas especificações do CSS, mas a comunidade costuma intervir.
Sem falar, é claro, que é bem mais fácil pedir ajuda se realmente for preciso, e muito mais conhecimento já foi gerado a respeito. É só comparar os números do StackOverflow entre Stitches e Tailwind.
Contras
É claro que o Tailwind tem sua parcela de problemas, ou metade dos desenvolvedores que trabalham com Front-End não o odiariam. Coloquei as críticas mais comuns abaixo (mas admito que, apesar de todos os pesares, e são muitos, gosto de usá-lo).
Pouca visibilidade no DevTools
Não tem muito o que dizer aqui, o DevTools é feito para facilitar a visualização dos estilos feitos por uma classe. Navegar os estilos de classes utilitárias através dele é uma experiência consistentemente miserável, principalmente se você já for acostumado com a forma mais comum.
É importante dizer que existem extensões, voltadas apenas para melhorar esse aspecto da DX do Tailwind. Nunca as usei, então não posso falar da qualidade, porém o fato de ser uma dor forte o bastante para estimular a criação desses produtos para mim diz muita coisa.
Markup esteticamente desagradável
Qualquer um que já esteja presente nas discussões atuais de web dev tem uma chance alta de ter visto essa imagem:
Para ser totalmente transparente, duas coisas são verdadeiras: 1) muita coisa precisa dar errado para chegar a esse ponto; 2) eu mesmo já escrevi código que já chegou a esse ponto. 🥲
Até onde a minha experiência vai, esses exemplos mais dramáticos podem ser evitados ao adotar boas práticas com o Tailwind. Apesar de isso não ser óbvio, existem casos — como esse — onde é melhor abstrair os estilos em um arquivo CSS-in-JS do Tailwind ou usar a diretiva @apply
para declarar estilos específicos, geralmente quando eles se tornam muito grandes ou com muitos níveis de aninhamento.
Isso dito, os detratores estão corretos em dizer que utilizar o Tailwind pode facilmente desembocar em um markup bem feio. Isso é inevitável, é da natureza da própria ferramenta e nós enquanto usuários podemos apenas esquivar parcialmente com extensões que ocultam os estilos ou adotar uma componentização radical.
Pode ser argumentado que existe uma um lado positivo que é a garantia de que os estilos têm um significado compreensível sem ser necessário a troca de contexto, mas sem dúvidas é algo a se considerar ao pensar no que é mais importante para a legibilidade de código do seu time.
Complexidade para criar estilos dinâmicos
Devido à natureza estática na compilação dos estilos no caso do Tailwind — que decididamente não utiliza JavaScript em tempo de execução para definir quais estilos carregar — não é tão simples assim. O exemplo abaixo ilustra um problema possível: e se eu quiser alterar a largura do meu botão de acordo com alguma condição?
O exemplo acima não funcionará como esperado. Apenas a condição verdadeira no carregamento inicial da página será considerada. Para mudar as classes de maneira dinâmica, é preciso adicionar o nome completo de cada uma de acordo com a condição desejada:
Isso é um desafio próprio do Tailwind, o que também significa que patterns específicos precisam ser adotados caso você queira adotar uma abordagem voltada para design system, que possa conter variantes, por exemplo. Uma opção é declarar um componente diferente para cada variante que você queira usar.
Essa abordagem por si só pode ser um pouco cansativa e meio que diminui a vantagem do Tailwind de evitar trocas de contexto; entretanto, também existe a opção usar uma biblioteca como o class-variance-authority
(ou cva
) para fazer o controle de variantes por você.
Isso tudo é uma grande desvantagem em relação a algo como o Stitches, em que variantes eram parte do workflow esperado do uso da biblioteca.
Investimento alto em aprender uma ferramenta só
Uma última consideração a fazer é que a produtividade ao utilizar o Tailwind é dependente de se acostumar com a linguagem utilizada por ele, principalmente na forma das classes que usamos para declarar os estilos mas também no ecossistema dele.
Acontece, é claro, que o Tailwind é uma biblioteca mantida por humanos, e é, naturalmente, imperfeita e propensa a mudanças. As escolhas de nomenclatura são demonstravelmente inconsistentes, as evoluções da ferramenta também arriscam trazer mudanças na forma de utilizá-lo.
Isso tudo é para dizer que o investimento inicial para ver retornos no uso dessa ferramenta é maior do que uma biblioteca como o Stitches onde o CSS é declarado de forma muito similar à tradicional, e — se tudo der certo — a tendência é de que haja a necessidade de novos investimentos conforme o tempo passa.
Em outras palavras: ao usar uma solução mais alinhada às especificações do CSS, a sua evolução está acoplada à evolução do CSS. Ao escolher evoluir junto com o Tailwind, você é obrigado a evoluir junto ao CSS e ao Tailwind. Isso também significa uma certa dificuldade de migração futura para outra solução, uma vez que é difícil sair do Tailwind uma vez que um grande projeto está usando ele (a não ser que LLMs como o Claude e o ChatGPT ajudem nisso no futuro).
Bom, e como eu fiquei com tudo isso?
“E se eu fizer a minha própria lib? 🤔”
… Ok, eu admito que não pensei nisso. É verdade que acabei criando uma lib para o Kamishibai, mas também é verdade que não era esse o meu plano inicial, e nem o meu pensamento inicial. O verdadeiro pontapé dessa empreitada foi o seguinte:
“E se eu tentasse criar um styles.tsx?”
De primeira, tudo o que eu queria de verdade era usar o pattern de declarar os estilos em um arquivo styles.ts
.
O fato é que quando os estilos são poucos, sem aninhamento e nem variantes, eu não via problema algum em simplesmente usar o Tailwind, mas a partir do momento em que essas condições começaram a se fazer presentes, senti que seria uma mão na roda separar as coisas. No meu caso, eu criei um pequeno design system para facilitar a minha própria jornada de desenvolvimento, e estava sentindo que a forma padrão de utilizar o Tailwind estava dificultando isso.
E foi aí que eu pensei: nada me impede de fazer isso no React, certo? Um componente declarado com o Tailwind é só um componente como qualquer outro. Facilmente posso declarar um arquivo styles.tsx
e imitar como fazia utilizando o Stitches. Assim, eu consigo ter o melhor dos dois mundos, correto?
Spoiler: nem tudo funcionou tão belamente assim. O primeiro ponto é que estou criando um elemento pronto, em vez de fornecer um elemento da DOM. É preciso corrigir essa implementação para que ela passe para frente os atributos que possam ser usados nesse elemento, bem como corrigir a tipagem do TypeScript para abarcá-los.
Já está melhorando, agora o meu autocomplete funciona quando eu tento alterar o type
desse botão, por exemplo, sem falar que se eu passar um onClick
, ele já é capaz de funcionar.
Bem, isso já resolve bastante coisa. De fato, os estilos já estão separados e isso aqui funciona…
… Mas e se eu tivesse mais? Sei lá, uma API parecida com a do Stitches, mas que permitisse declarar estilos usando o Tailwind, com autocomplete e tudo. E falando em autocomplete, poderia ter também nos atributos do elemento da DOM, e com uma forma mais padronizada de declarar variantes.
E se eu pudesse declarar o esse mesmo botão… assim?
Olha que API maneira. Ela é quase como utilizar o Stitches com classes do Tailwind. Também abstrai caso seja necessário utilizar uma ref
do React (como quando usando uma biblioteca como o react-hook-form
, por exemplo). Ela tem até o autocomplete de quais elementos da DOM são possíveis de serem declarados, assim como o Stitches!
Como eu fiz isso? Quais são as vantagens e desvantagens? Valeu a pena? Bem… acho que você vai se interessar pela segunda parte desse texto. 😉
Por enquanto é isso. Espero que tenha gostado dessa primeira parte; te espero na próxima, onde vamos analisar a minha própria lib CSS-in-JS um pouco mais de perto!