CSS-in-JS vs CSS Modules vs Vanilla CSS: Qual Estratégia de Estilização Escolher em 2025
O custo que ninguém mostra no tutorial de estilização
A escolha de estratégia de estilização parece uma decisão de gosto pessoal. Não é. Ela afeta bundle size, tempo de hidratação, cache de assets e a capacidade do time de refatorar componentes sem medo. E o cenário mudou: o React Server Components tornou CSS-in-JS runtime um problema real de performance, enquanto o CSS nativo ganhou features como @layer, nesting e container queries que eliminam boa parte da motivação original de soluções como styled-components.
Este post compara três estratégias com critérios explícitos, código funcional e uma matriz de decisão para você fechar a escolha sem ficar preso em thread do Twitter.
As três abordagens em código real
Antes de comparar, vale nivelar o que cada abordagem produz de fato. O componente é o mesmo: um card de produto com título, preço e badge de desconto.
CSS-in-JS com styled-components
// ProductCard.tsx
import styled from 'styled-components';
// Transient prop ($hasDiscount) evita que a prop vaze para o DOM
const Badge = styled.span<{ $hasDiscount: boolean }>`
display: ${({ $hasDiscount }) => ($hasDiscount ? 'inline-block' : 'none')};
background: #e53e3e;
color: #fff;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75rem;
`;
const Card = styled.article`
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 16px;
transition: box-shadow 0.2s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
`;
const Price = styled.p`
font-size: 1.25rem;
font-weight: 700;
color: #1a202c;
`;
interface ProductCardProps {
title: string;
price: number;
discountPercent?: number;
}
export function ProductCard({ title, price, discountPercent }: ProductCardProps) {
const hasDiscount = (discountPercent ?? 0) > 0;
return (
<Card>
<h3>{title}</h3>
<Price>R$ {price.toFixed(2)}</Price>
<Badge $hasDiscount={hasDiscount}>{discountPercent}% OFF</Badge>
</Card>
);
}
CSS Modules
/* ProductCard.module.css */
.card {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 16px;
transition: box-shadow 0.2s ease;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.price {
font-size: 1.25rem;
font-weight: 700;
color: #1a202c;
}
.badge {
display: none;
background: #e53e3e;
color: #fff;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75rem;
}
/* Classe condicional separada: mais fácil de rastrear no DevTools */
.badgeVisible {
display: inline-block;
}
// ProductCard.tsx
import styles from './ProductCard.module.css';
interface ProductCardProps {
title: string;
price: number;
discountPercent?: number;
}
export function ProductCard({ title, price, discountPercent }: ProductCardProps) {
const hasDiscount = (discountPercent ?? 0) > 0;
return (
<article className={styles.card}>
<h3>{title}</h3>
<p className={styles.price}>R$ {price.toFixed(2)}</p>
<span className={`${styles.badge} ${hasDiscount ? styles.badgeVisible : ''}`}>
{discountPercent}% OFF
</span>
</article>
);
}
Vanilla CSS com data attributes
/* product-card.css */
/* @layer garante que estilos de componente não conflitem com reset global */
@layer components {
.product-card {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 16px;
transition: box-shadow 0.2s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
}
.product-card__price {
font-size: 1.25rem;
font-weight: 700;
color: #1a202c;
}
.product-card__badge {
display: none;
background: #e53e3e;
color: #fff;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75rem;
/* data attribute como API de estado: semântico e pesquisável */
&[data-visible='true'] {
display: inline-block;
}
}
}
// ProductCard.tsx
import './product-card.css';
interface ProductCardProps {
title: string
---
Leia o artigo completo em [https://www.vivodecodigo.com.br/react/css-in-js-vs-css-modules-vs-vanilla-css-estrategia-estilizacao-2025](https://www.vivodecodigo.com.br/react/css-in-js-vs-css-modules-vs-vanilla-css-estrategia-estilizacao-2025)