Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

CSS no React: módulos, Tailwind ou styled-components

As quatro formas de estilizar componente no React, o que cada uma custa no bundle final e a tabela de quando escolher cada uma delas.

Rodolfo Mori15 min de leitura

Você monta o componente, abre o navegador e surge a primeira dúvida: devo usar style, importar um arquivo CSS ou instalar alguma biblioteca? Ao final desta lição, você saberá aplicar cada opção, reconhecer os erros silenciosos mais comuns e escolher um caminho sem depender de torcida por ferramenta.

O React não cria um novo tipo de CSS. Ele renderiza elementos do navegador e permite ligar as regras a esses elementos principalmente por className ou pela prop style. Em projetos reais também aparecem CSS Modules, Tailwind e styled-components; eles mudam onde você escreve e processa as regras, não a função básica do CSS.

Pense no uniforme de uma equipe. O inline leva a instrução costurada na própria peça; CSS Modules guarda cada molde com uma etiqueta local; Tailwind oferece um catálogo de etiquetas prontas; styled-components leva uma máquina que monta regras durante a execução. A comparação ajuda a localizar o estilo, mas tem um limite: no navegador, cascade, herança e especificidade continuam sendo CSS de verdade.

Para enxergar a diferença sem trocar também o problema, vamos vestir o mesmo CardLivro da Livraria Sete Ventos de quatro maneiras. Depois compararemos os builds medidos na mesma máquina; primeiro, porém, vamos entender o mínimo que aparece na tela.

Como o CSS chega a um componente React?

Ele chega diretamente pelo objeto da prop style ou indiretamente por uma string em className que aponta para regras CSS. O conteúdo do card abaixo é o mesmo nas quatro versões; só o caminho do estilo muda:

jsx
export default function CardLivro({ titulo, autor, preco, esgotado }) {
  return (
    <article>
      <span>{esgotado ? 'Esgotado' : 'Em estoque'}</span>
      <h3>{titulo}</h3>
      <p>{autor}</p>
      <strong>R$ {preco.toFixed(2)}</strong>
      <button disabled={esgotado}>Adicionar à sacola</button>
    </article>
  );
}

O card recebe dados por props e usa um ternário para decidir o texto do selo. O termo markup significa a estrutura de elementos que será convertida em HTML. Para isolar apenas a forma de estilizar, o próximo teste renderiza o mesmo selo nas quatro abordagens. Como ele já importa styled-components, abra o terminal na pasta do projeto e rode npm install styled-components antes de copiá-lo; CSS Modules já é processado pelo Vite e não pede outro pacote.

jsx
import { renderToStaticMarkup as html } from 'react-dom/server';
import styled from 'styled-components';
import estilos from './Selo.module.css';

const SeloStyled = styled.span`
  font-size: 12px;
  text-transform: uppercase;
  color: #a12d2d;
`;

console.log('inline  ', html(
  <span style={{ fontSize: 12, textTransform: 'uppercase', color: '#a12d2d' }}>Esgotado</span>
));
console.log('modules ', html(<span className={estilos.selo}>Esgotado</span>));
console.log('tailwind', html(<span className="text-xs uppercase text-red-800">Esgotado</span>));
console.log('styled  ', html(<SeloStyled>Esgotado</SeloStyled>));
inline <span style="font-size:12px;text-transform:uppercase;color:#a12d2d">Esgotado</span> modules <span class="_selo_frpim_1">Esgotado</span> tailwind <span class="text-xs uppercase text-red-800">Esgotado</span> styled <span class="sc-bdvwhi fWSUzh">Esgotado</span>

A primeira linha coloca declarações dentro do atributo style. CSS Modules entrega uma classe transformada pelo build; Tailwind mantém no HTML os nomes das utilidades escritas; styled-components produz classes próprias. A aparência pode ser igual, mas o lugar onde você procura uma regra depois é diferente.

no build no navegador
plaintext
<text x="8" y="64" font-size="12" fill="currentColor">style inline</text>
<rect x="370" y="46" width="258" height="30" rx="6" stroke="currentColor" fill="none" />
<text x="382" y="65" font-size="11" fill="currentColor">atributo style em cada elemento</text>

<text x="8" y="109" font-size="12" fill="currentColor">CSS Modules</text>
<rect x="150" y="91" width="200" height="30" rx="6" stroke="currentColor" fill="none" />
<text x="162" y="110" font-size="11" fill="currentColor">renomeia e junta num .css</text>
<line x1="350" y1="106" x2="368" y2="106" stroke="currentColor" fill="none" />
<polyline points="362,102 368,106 362,110" stroke="currentColor" fill="none" />
<rect x="370" y="91" width="258" height="30" rx="6" stroke="currentColor" fill="none" />
<text x="382" y="110" font-size="11" fill="currentColor">baixa uma folha pronta</text>

<text x="8" y="154" font-size="12" fill="currentColor">Tailwind</text>
<rect x="150" y="136" width="200" height="30" rx="6" stroke="currentColor" fill="none" />
<text x="162" y="155" font-size="11" fill="currentColor">varre o JSX e gera as classes</text>
<line x1="350" y1="151" x2="368" y2="151" stroke="currentColor" fill="none" />
<polyline points="362,147 368,151 362,155" stroke="currentColor" fill="none" />
<rect x="370" y="136" width="258" height="30" rx="6" stroke="currentColor" fill="none" />
<text x="382" y="155" font-size="11" fill="currentColor">baixa uma folha pronta</text>

<text x="8" y="199" font-size="12" fill="currentColor">styled-components</text>
<rect x="370" y="181" width="258" height="30" rx="6" stroke="currentColor" fill="none" />
<text x="382" y="200" font-size="11" fill="currentColor">a lib monta as regras e injeta &lt;style&gt;</text>

Leia o diagrama da esquerda para a direita: CSS Modules e Tailwind preparam uma folha no build; o inline escreve no elemento; styled-components precisa de código da biblioteca em execução para criar e inserir regras. “Build” é a etapa que transforma o projeto em arquivos de produção. Esse mapa será nossa referência quando compararmos tamanho e manutenção.

O desenho representa especificamente este laboratório Vite, renderizado no cliente. styled-components também oferece ServerStyleSheet para coletar CSS no servidor e reidratar as regras no navegador; portanto, “injeta no navegador” não descreve toda arquitetura possível com a biblioteca.

Quando vale usar style inline no React?

Use style principalmente quando um valor nasce no JavaScript durante a execução, como a largura de um progresso ou uma posição calculada. No JSX, essa prop recebe um objeto, não a string usada no HTML. Os nomes viram camelCase: background-color passa a ser backgroundColor.

jsx
const estilos = {
  card: {
    border: '1px solid #d8d2c4',
    borderRadius: '12px',
    padding: '16px',
    background: '#fffdf7',
  },
  botao: { width: '100%', background: '#1f6f43', color: '#fff' },
};

export default function CardLivro({ titulo }) {
  return (
    <article style={estilos.card}>
      <h3>{titulo}</h3>
      <button style={estilos.botao}>Adicionar à sacola</button>
    </article>
  );
}

O objeto está fora do componente porque esses valores são fixos. Criá-lo dentro da função produz uma nova referência em cada re-render. Isso não é um erro por si só, mas pode invalidar uma otimização se o objeto for passado a um filho memoizado. Comece simples e só trate essa referência quando houver uma necessidade medida.

Valores numéricos recebem px nas propriedades que esperam comprimento; as propriedades definidas como sem unidade continuam sem sufixo. Veja a conversão:

jsx
console.log(
  html(
    <p style={{ fontSize: 18, marginTop: 8, lineHeight: 1.4, zIndex: 3, opacity: 0.8 }}>
      Vidas Secas
    </p>
  )
);
<p style="font-size:18px;margin-top:8px;line-height:1.4;z-index:3;opacity:0.8">Vidas Secas</p>

Na saída, fontSize: 18 virou font-size:18px, enquanto lineHeight: 1.4, zIndex: 3 e opacity: 0.8 permaneceram sem unidade. Essa diferença vem da lista de propriedades sem unidade reconhecida pelo React.

O limite aparece quando a regra depende de um seletor. O atributo style não expressa sozinho :hover, ::before nem media query. Tentar colocar uma pseudo-classe como propriedade do objeto gera esta saída:

jsx
const botao = {
  background: '#1f6f43',
  color: '#fff',
  ':hover': { background: '#175734' },
};

console.log(html(<button style={botao}>Adicionar à sacola</button>));
<button style="background:#1f6f43;color:#fff;:hover:[object Object]">Adicionar à sacola</button>

O trecho :hover:[object Object] é a pista do problema: o objeto aninhado virou texto, a declaração ficou inválida e o navegador a ignorou. A correção é manter o valor dinâmico no inline e mover estados de seletor para uma classe CSS.

Experimente você mesmo

Antes de executar, preveja duas coisas: qual valor receberá px e se o botão mudará de cor ao passar o mouse. Coloque o componente inline no App.jsx e renderize-o como <CardLivro titulo="Torto Arado" />. Rode npm run dev e troque padding: '16px' por padding: 24. Depois teste o objeto com :hover e inspecione o atributo style no DevTools. Você deve ver 24px e confirmar que a pseudo-classe não funciona ali.

Como o CSS Modules evita conflito entre nomes?

O CSS Modules dá escopo local às classes ao transformar seus nomes durante o build. No Vite, basta terminar o arquivo com .module.css; importar esse arquivo devolve um objeto que liga o nome legível à classe gerada.

É como guardar duas chaves chamadas “card” em chaveiros identificados: você pode usar .card em componentes diferentes sem depender de um nome global único. A analogia termina no nome, pois propriedades herdadas e variáveis CSS ainda podem atravessar a árvore normalmente.

css
.card {
  border: 1px solid #d8d2c4;
  border-radius: 12px;
  padding: 16px;
  background: #fffdf7;
  font-family: system-ui, sans-serif;
}

.selo {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: #2f6f3e;
}

.seloEsgotado {
  color: #a12d2d;
}

.botao:disabled {
  background: #b7b1a4;
  cursor: not-allowed;
}

As regras omitidas seguem o mesmo CSS conhecido: .titulo, .autor, .preco, .botao e .botao:hover. Pseudo-classes e media queries continuam válidas. Agora o componente usa o objeto importado para preencher className:

jsx
import estilos from './CardLivro.module.css';

export default function CardLivro({ titulo, autor, preco, esgotado }) {
  return (
    <article className={estilos.card}>
      <span className={`${estilos.selo} ${esgotado ? estilos.seloEsgotado : ''}`}>
        {esgotado ? 'Esgotado' : 'Em estoque'}
      </span>
      <h3 className={estilos.titulo}>{titulo}</h3>
      <p className={estilos.autor}>{autor}</p>
      <strong className={estilos.preco}>R$ {preco.toFixed(2)}</strong>
      <button className={estilos.botao} disabled={esgotado}>
        Adicionar à sacola
      </button>
    </article>
  );
}

No build deste laboratório, o CSS de produção saiu minificado e com os nomes transformados:

._card_14rzk_1{background:#fffdf7;border:1px solid #d8d2c4;border-radius:12px;padding:16px;font-family:system-ui,sans-serif}._selo_14rzk_9{text-transform:uppercase;letter-spacing:.06em;color:#2f6f3e;font-size:12px}._seloEsgotado_14rzk_16{color:#a12d2d}._titulo_14rzk_20{margin:8px 0 2px;font-size:18px}._autor_14rzk_25{color:#6b6455;margin:0;font-size:14px}._preco_14rzk_31{margin-top:12px;font-size:20px;display:block}._botao_14rzk_37{color:#fff;cursor:pointer;background:#1f6f43;border:none;border-radius:8px;width:100%;margin-top:12px;padding:10px}._botao_14rzk_37:hover{background:#175734}._botao_14rzk_37:disabled{cursor:not-allowed;background:#b7b1a4}

Na saída, .card aparece como ._card_14rzk_1, e o componente recebe esse nome por estilos.card. O formato exato é detalhe do empacotador: não escreva nem procure esse nome gerado à mão. Continue usando o objeto importado.

Um import como import './global.css' é diferente: o Vite injeta aquelas regras na página e elas continuam globais. Já import estilos from './CardLivro.module.css' retorna o mapa local. Você pode usar variáveis CSS dos temas globais dentro do módulo e, quando realmente houver uma regra-base local, o recurso composes do CSS Modules permite composição. Para iniciantes, uma classe explícita e variáveis CSS costumam ser mais fáceis de rastrear.

Por que uma classe do CSS Module pode desaparecer?

Ela desaparece quando a chave consultada no objeto não existe. Um nome em kebab-case não corresponde automaticamente ao acesso camelCase na configuração padrão deste exemplo:

jsx
import estilos from './CardLivro.module.css'; // tem .selo-esgotado

console.log(Object.keys(estilos));
console.log(html(<span className={estilos.seloEsgotado}>Esgotado</span>));
[ 'card', 'selo-esgotado' ] <span>Esgotado</span>

A lista mostra 'selo-esgotado', mas o código procurou seloEsgotado. O valor virou undefined; por isso a segunda linha é apenas <span>Esgotado</span>, sem atributo class. Corrija escrevendo a classe em camelCase, usando estilos['selo-esgotado'] ou configurando a convenção de nomes no Vite.

Experimente você mesmo

Preveja o resultado antes de salvar: o selo ficará verde ou vermelho? Crie CardLivro.module.css com as classes do exemplo, importe-o no componente e rode o card no App.jsx como <CardLivro titulo="Torto Arado" autor="Itamar Vieira Junior" preco={54.9} esgotado={true} />. Execute npm run dev. Depois renomeie .seloEsgotado para .selo-esgotado sem alterar o JSX. Abra o console, confira Object.keys(estilos) e aplique uma das três correções acima. O exercício termina quando a classe gerada de esgotado reaparece no atributo class e o selo volta a ficar vermelho.

Como usar Tailwind no Vite sem perder classes?

No Tailwind 4, você pode usar o plugin oficial do Vite e importar o framework no CSS. As classes precisam aparecer completas nos arquivos que o Tailwind examina; montar apenas pedaços do nome em JavaScript pode fazer a regra sumir do build.

Na pasta do projeto Vite, rode npm install tailwindcss @tailwindcss/vite. Depois adicione o plugin ao vite.config.js, como no primeiro bloco abaixo. No arquivo de entrada src/index.css, coloque a importação do segundo bloco e confirme que src/main.jsx ainda contém import './index.css'; sem esse último import, o CSS é criado, mas não entra na página.

js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({ plugins: [react(), tailwindcss()] });
css
@import "tailwindcss";

Depois da configuração, você combina utilidades dentro de className. Cada nome representa uma regra pequena: p-4 adiciona espaçamento, rounded-xl arredonda as bordas e disabled:bg-stone-400 vale no estado desabilitado.

jsx
export default function CardLivro({ titulo, autor, preco, esgotado }) {
  return (
    <article className="rounded-xl border border-stone-300 bg-stone-50 p-4 font-sans">
      <span
        className={`text-xs uppercase tracking-wider ${
          esgotado ? 'text-red-800' : 'text-green-800'
        }`}
      >
        {esgotado ? 'Esgotado' : 'Em estoque'}
      </span>
      <h3 className="mt-2 mb-0.5 text-lg">{titulo}</h3>
      <p className="text-sm text-stone-500">{autor}</p>
      <strong className="mt-3 block text-xl">R$ {preco.toFixed(2)}</strong>
      <button
        className="mt-3 w-full cursor-pointer rounded-lg bg-green-800 p-2.5 text-white hover:bg-green-900 disabled:cursor-not-allowed disabled:bg-stone-400"
        disabled={esgotado}
      >
        Adicionar à sacola
      </button>
    </article>
  );
}

Aqui mora o erro mais comum: o Tailwind lê os arquivos como texto para detectar classes, sem executar o componente nem compreender a interpolação. O teste abaixo troca nomes completos por text-${cor}-800 e compara os builds:

jsx
export default function CardLivro({ titulo, esgotado }) {
  const cor = esgotado ? 'red' : 'green';
  return (
    <article className="rounded-xl border border-stone-300 p-4">
      <span className={`text-xs uppercase text-${cor}-800`}>
        {esgotado ? 'Esgotado' : 'Em estoque'}
      </span>
      <h3 className="mt-2 text-lg">{titulo}</h3>
    </article>
  );
}
bash
grep -o "\.text-[a-z]*-800" 03-tailwind/dist/assets/*.css
grep -o "\.text-[a-z]*-800" 06-tw-dinamico/dist/assets/*.css
$ grep -o "\.text-[a-z]*-800" 03-tailwind/dist/assets/*.css .text-green-800 .text-red-800 $ grep -o "\.text-[a-z]*-800" 06-tw-dinamico/dist/assets/*.css (exit 1)

O primeiro grep encontrou .text-green-800 e .text-red-800. O segundo terminou com (exit 1), que nesse comando significa “nenhuma correspondência”. Não é o ternário que resolve: são os dois nomes completos visíveis no arquivo. A correção é mapear cada estado para uma string completa. Se nenhuma utilidade for gerada, revise antes a configuração do projeto com Vite.

Experimente você mesmo

Antes de rodar, preveja qual versão deixará o selo colorido. Execute os dois componentes renderizando, por exemplo, <CardLivro titulo="Torto Arado" autor="Itamar Vieira Junior" preco={54.9} esgotado={false} />. Faça npm run build e procure text-red-800 no CSS de dist. Em seguida, substitua a interpolação por um objeto como const cores = { red: 'text-red-800', green: 'text-green-800' } e use cores[cor]. O teste está certo quando as duas classes completas voltarem ao arquivo gerado.

Quando styled-components pode fazer sentido?

Ele pode fazer sentido quando o projeto já usa CSS-in-JS ou precisa ligar temas e props às regras com a API da biblioteca. Em vez de aplicar apenas uma classe a uma tag, você cria um componente estilizado, como Card ou Selo:

Se você chegou direto nesta seção e ainda não instalou a biblioteca, rode npm install styled-components na pasta do projeto antes de salvar o componente.

jsx
import styled from 'styled-components';

const Card = styled.article`
  border: 1px solid #d8d2c4;
  border-radius: 12px;
  padding: 16px;
  background: #fffdf7;
`;

const Selo = styled.span`
  font-size: 12px;
  text-transform: uppercase;
  color: ${(props) => (props.$esgotado ? '#a12d2d' : '#2f6f3e')};
`;

const Titulo = styled.h3`
  font-size: 18px;
  margin: 8px 0 2px;
`;

const Preco = styled.strong`
  display: block;
  margin-top: 12px;
  font-size: 20px;
`;

export default function CardLivro({ titulo, preco, esgotado }) {
  return (
    <Card>
      <Selo $esgotado={esgotado}>{esgotado ? 'Esgotado' : 'Em estoque'}</Selo>
      <Titulo>{titulo}</Titulo>
      <Preco>R$ {preco.toFixed(2)}</Preco>
    </Card>
  );
}

Card e Selo são componentes React e recebem props. A interpolação lê $esgotado para escolher a cor. O prefixo $ marca uma prop transiente: ela participa do estilo, mas não é encaminhada ao elemento HTML. Sem o prefixo, o teste reproduz este aviso:

jsx
const Selo = styled.span`
  color: ${(props) => (props.esgotado ? '#a12d2d' : '#2f6f3e')};
`;

console.log(html(<Selo esgotado={true}>Esgotado</Selo>));
Received `true` for a non-boolean attribute `esgotado`.

If you want to write it to the DOM, pass a string instead: esgotado=“true” or esgotado={value.toString()}. <span class=“sc-bdvwhi iEISLD”>Esgotado</span>

O aviso diz que true foi enviado para um atributo não booleano chamado esgotado. O <span> ainda foi renderizado com classes, mas o console denuncia que uma prop interna vazou para o DOM. Neste caso, volte a $esgotado; em composições mais avançadas, a biblioteca também oferece shouldForwardProp.

Essa API traz código adicional no benchmark abaixo, feito especificamente com styled-components 6.5.3. O custo e a estratégia podem mudar conforme versão, SSR, compilação e arquitetura; portanto, o número do laboratório não é uma lei para todo projeto.

Como montar classes condicionais sem criar lixo no HTML?

Produza uma única string contendo apenas os nomes ativos. Em CSS Modules e Tailwind, um template literal funciona para casos pequenos, mas misturar && e valores booleanos pode inserir texto indesejado:

jsx
import estilos from './Selo.module.css'; // tem .selo, .esgotado e .novo

function Selo({ esgotado, novo }) {
  return (
    <span className={`${estilos.selo} ${esgotado && estilos.esgotado} ${novo ? estilos.novo : ''}`}>
      Em estoque
    </span>
  );
}

console.log(html(<Selo esgotado={false} novo={false} />));
<span class="_selo_1ultd_1 false ">Em estoque</span>

A saída confirma a armadilha: o booleano false virou literalmente a classe false, e o valor vazio deixou um espaço extra. Isso não costuma mudar a aparência, mas suja o HTML e dificulta o diagnóstico. Uma função como clsx descarta valores falsos e junta só as classes presentes:

Instale essa função auxiliar uma vez com npm install clsx; depois o import do próximo bloco passa a resolver normalmente.

jsx
import clsx from 'clsx';
import estilos from './Selo.module.css';

function Selo({ esgotado, novo }) {
  return (
    <span className={clsx(estilos.selo, esgotado && estilos.esgotado, novo && estilos.novo)}>
      Em estoque
    </span>
  );
}

console.log(html(<Selo esgotado={false} novo={false} />));
<span class="_selo_1ultd_1">Em estoque</span>

Agora a saída contém apenas _selo_1ultd_1: sem false e sem espaço final. Você pode fazer um ternário explícito sem biblioteca ou usar clsx quando a lista crescer. Com Tailwind, tailwind-merge também pode eliminar utilidades que o pacote reconhece como conflitantes.

Essas funções organizam a string, mas não desligam a cascade. Se duas regras CSS válidas competirem, origem, camada, importância, especificidade e ordem ainda decidem a vencedora. Até dois arquivos globais importados em pontos diferentes podem mudar o resultado pela ordem de carregamento. Revise a base de JSX no React e, para conflitos reais, consulte a especificidade do CSS.

Quanto cada abordagem adicionou a este bundle?

Neste laboratório, CSS Modules ficou próximo do inline, Tailwind acrescentou CSS e styled-components acrescentou mais JavaScript. Essa é uma comparação controlada, não uma previsão para qualquer aplicação.

Os quatro projetos têm o mesmo index.html, o mesmo App.jsx e três livros na tela, com React 19.2.8 e Vite 8.2.2. O comando abaixo gera o build de produção de cada versão:

Esse comando pressupõe quatro pastas irmãs chamadas 01-inline, 02-modules, 03-tailwind e 04-styled; ele é o registro do laboratório executado para esta comparação. Para repetir, duplique seu projeto Vite quatro vezes, aplique em cada cópia a versão ensinada na seção correspondente e rode o comando a partir da pasta que contém as quatro. Se preferir trabalhar em uma única cópia, rode npm run build antes de trocar de abordagem e anote cada resultado.

bash
for d in 01-inline 02-modules 03-tailwind 04-styled; do
  echo "===== $d ====="
  (cd $d && npx vite build)
done
===== 01-inline ===== dist/index.html 0.23 kB │ gzip: 0.19 kB dist/assets/index-DlIm8hOe.js 191.71 kB │ gzip: 60.57 kB ✓ built in 49ms ===== 02-modules ===== dist/index.html 0.30 kB │ gzip: 0.23 kB dist/assets/index-LcARsVcy.css 0.65 kB │ gzip: 0.37 kB dist/assets/index-DkAIXM7b.js 191.32 kB │ gzip: 60.38 kB ✓ built in 70ms ===== 03-tailwind ===== dist/index.html 0.30 kB │ gzip: 0.23 kB dist/assets/index-DI2Hv_qn.css 6.69 kB │ gzip: 2.19 kB dist/assets/index-Di6clDw3.js 191.41 kB │ gzip: 60.45 kB ✓ built in 68ms ===== 04-styled ===== dist/index.html 0.23 kB │ gzip: 0.19 kB dist/assets/index-CIkooBDY.js 218.36 kB │ gzip: 71.28 kB ✓ built in 49ms

Cada grupo da saída lista o HTML e os arquivos JS ou CSS gerados. A coluna gzip aproxima o tamanho transferido quando o servidor usa essa compressão; o tempo em milissegundos mede apenas aquela execução. Organizando os valores:

abordagem JS (gzip) CSS (gzip) total gzip diferença
style inline 60,57 kB 60,57 kB referência
CSS Modules 60,38 kB 0,37 kB 60,75 kB +0,18 kB
Tailwind 60,45 kB 2,19 kB 62,64 kB +2,07 kB
styled-components 71,28 kB 71,28 kB +10,71 kB

Leia sempre a diferença contra a primeira linha:

  • CSS Modules somou 0,18 kB gzip neste card; o escopo foi resolvido no build.
  • Tailwind somou 2,07 kB gzip. Um teste separado só com p-4 gerou 1,44 kB gzip entre preflight e variáveis; o restante depende das utilidades usadas aqui.
  • styled-components somou 10,71 kB gzip ao JS nesta configuração e versão.

Os tempos de build não sustentam uma escolha neste exemplo pequeno. Em cinco execuções após aquecimento, no MacBook usado no teste, as médias foram 45,6 ms, 48,8 ms, 53,4 ms e 49,4 ms. A diferença ficou próxima da variação entre rodadas; para um projeto real, repita a medição na própria base.

O que aconteceu quando o laboratório ganhou seis componentes?

O CSS dos módulos cresceu mais em bytes brutos, enquanto as utilidades do Tailwind foram mais reaproveitadas neste desenho específico. Isso mostra a curva deste experimento, não uma regra universal sobre projetos grandes.

O card foi duplicado como CardLivro, CardAutor, CardColecao, CardEditora, CardSelo e CardResenha. Cada um recebeu raio entre 10 px e 18 px e padding entre 14 px e 22 px. Nos módulos, o laboratório repetiu o conjunto de regras em seis arquivos; no Tailwind, trocou utilidades como rounded-xl p-4 por valores de raio e espaço diferentes. O novo build produziu:

===== 07-modules-6 ===== dist/assets/index-bulJsvPi.css 3.28 kB │ gzip: 0.57 kB dist/assets/index-CzH8pYX9.js 194.56 kB │ gzip: 60.71 kB ===== 08-tailwind-6 ===== dist/assets/index-DWsp-i-C.css 7.03 kB │ gzip: 2.25 kB dist/assets/index-D4Mb5bOP.js 195.06 kB │ gzip: 60.57 kB

Na saída, o CSS dos módulos foi de 0,65 kB para 3,28 kB, pois esta implementação repetiu as regras em cada arquivo. O CSS do Tailwind foi de 6,69 kB para 7,03 kB; o build reutilizou utilidades já existentes e acrescentou oito combinações. Em gzip, porém, os módulos ainda ficaram menores: 0,57 kB contra 2,25 kB.

Há outras arquiteturas possíveis. Os módulos poderiam compartilhar uma classe base com composes, um arquivo comum ou variáveis CSS; o Tailwind poderia ganhar muitas combinações únicas. Portanto, compare também legibilidade, cache, manutenção e requisitos do time. Independentemente da ferramenta, regras que atingem o mesmo elemento continuam sujeitas à cascade.

Qual abordagem escolher para começar?

Para o primeiro projeto, CSS Modules é um ponto de partida seguro porque mantém o CSS reconhecível e reduz colisões de nomes. Depois, deixe os requisitos — e não a popularidade — indicarem se vale usar inline, Tailwind ou styled-components.

A tabela resume hipóteses iniciais com base neste laboratório. Leia “escolha” como a primeira opção a avaliar, não como resposta automática:

situação escolha por quê
primeiro projeto React, estudando CSS Modules é CSS que você já sabe, com escopo de graça e zero instalação
valor calculado em tempo de execução style inline largura de barra, posição de tooltip, cor vinda da API
time com mais de duas pessoas no mesmo layout Tailwind ninguém precisa inventar nome de classe nem caçar arquivo
design system com tema trocável no clique styled-components prop e tema entram no CSS sem duplicar regra
menor bundle com hover e media queries neste laboratório CSS Modules +0,18 kB gzip, sem abrir mão de seletores CSS

Guarde quatro sinais de diagnóstico: inline não representa pseudo-classes; CSS Modules pode devolver undefined quando a chave está errada; Tailwind exige nomes completos detectáveis; props internas do styled-components devem ser filtradas. Nenhum deles transforma uma ferramenta em vencedora universal.

Como transformar esta comparação em prática?

Implemente o mesmo estado esgotado em duas abordagens e compare o resultado. Você terminará com evidência de manutenção e bundle, em vez de apenas uma preferência.

Se o projeto ainda não existe, comece por criar um projeto React com Vite. Escreva o card da Livraria Sete Ventos sem copiar, altere a cor do selo nas duas versões e rode vite build. Anote JS e CSS em gzip e marque onde faria a próxima mudança. Se className ainda parecer estranho, revise JSX no React.

Este artigo fecha a sequência atual da trilha de React; React Router apareceu no passo anterior. A ponte agora é um projeto pequeno: monte uma estante com três cards, uma página de detalhes e um tema próprio. A melhor abordagem para esse projeto é a que cumpre o requisito com um lugar previsível para encontrar e corrigir cada regra.

Prefere aprender em vídeo?

Tem aula sobre este assunto no nosso canal.

Ver todos os vídeos do canal
  • react
  • css
  • css modules
  • tailwind
  • styled-components

Perguntas frequentes

Dá para misturar mais de uma abordagem no mesmo projeto?
Dá, e acontece o tempo todo — Tailwind no layout e um CSS Module para um componente com muitas regras é uma combinação comum. O que atrapalha é misturar sem critério: quando metade do time escreve classe utilitária e a outra metade escreve arquivo, ninguém sabe mais onde procurar a regra que está vencendo.
CSS Modules funciona fora do Vite?
Funciona. É um recurso do empacotador, não do React. Vite, Next.js, Rspack e webpack com css-loader entendem o sufixo .module.css sem plugin extra. Muda o formato do nome gerado, não a ideia.
Preciso de arquivo de configuração para o Tailwind 4?
Não. Na versão 4 o tailwind.config.js deixou de ser obrigatório: você escreve @import "tailwindcss" no CSS e personaliza cores e escalas com o bloco @theme dentro do próprio arquivo CSS.
styled-components ainda faz sentido em 2026?
Faz em dois cenários: design system que precisa de tema trocável em tempo de execução, e projeto legado que já é inteiro assim. Para um projeto novo sem esses requisitos, o mesmo resultado sai com CSS Modules e variáveis CSS, sem os 10 kB de biblioteca no bundle.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, Vite 8.2.2, React 19.2.8, Tailwind 4.3.3, styled-components 6.5.3, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. React — Common components: the style prop — react.dev
  2. Vite — CSS Modules — vite.dev
  3. Tailwind CSS — Detecting classes in source files — tailwindcss.com
  4. styled-components — Transient props — styled-components.com
  5. styled-components — Server-side rendering — styled-components.com

Continue por aqui