BEM, OOCSS ou Utility-First: Como Organizar Seu CSS Sem Enlouquecer

Conteúdo técnico toda semana
Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.
Sem spam. Cancele a qualquer momento com 1 clique.
Neste artigo
O Problema Que Todo Projeto Enfrenta
CSS é fácil de escrever. CSS é impossível de manter.
Essa frase resume a dor de qualquer equipe que já trabalhou em um projeto com mais de 50 componentes. Seletores conflitam. Especificidade vira uma guerra. !important aparece como "solução temporária" que nunca sai.
A raiz do problema não é o CSS em si. É a falta de convenção. Sem uma metodologia clara, cada dev escreve do seu jeito. O resultado? Um stylesheet de 8.000 linhas que ninguém ousa refatorar.
Três metodologias dominam o mercado: BEM, OOCSS e Utility-First. Cada uma resolve o caos de um jeito diferente. Vamos dissecar as três.
BEM: Block, Element, Modifier
BEM nasceu no Yandex, o Google russo. A ideia é simples: nomear classes CSS seguindo uma convenção rígida que elimina ambiguidade.
A estrutura é esta:
- Block: componente independente (
.card) - Element: parte interna do bloco (
.card__title) - Modifier: variação do bloco ou elemento (
.card--featured)
Anatomia de um Componente BEM
<article class="card card--featured">
<img class="card__image" src="/hero.jpg" alt="Post destaque" />
<div class="card__content">
<h2 class="card__title">Título do Post</h2>
<p class="card__excerpt">Resumo breve do conteúdo...</p>
<a class="card__link card__link--primary" href="/post">
Leia mais
</a>
</div>
</article>O CSS correspondente fica assim:
/* Block */
.card {
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
background: #fff;
}
/* Modifier do Block */
.card--featured {
border-color: #3b82f6;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
}
/* Elements */
.card__image {
width: 100%;
height: 200px;
object-fit: cover;
}
.card__content {
padding: 1.5rem;
}
.card__title {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 0.5rem;
color: #1e293b;
}
.card__excerpt {
font-size: 0.875rem;
color: #64748b;
line-height: 1.6;
}
.card__link {
display: inline-block;
margin-top: 1rem;
font-weight: 600;
text-decoration: none;
}
/* Modifier do Element */
.card__link--primary {
color: #3b82f6;
}
.card__link--primary:hover {
color: #1d4ed8;
}Por Que BEM Funciona
Cada classe é autoexplicativa. Olhando .card__title--highlighted, você sabe exatamente:
- A qual componente pertence (
card) - Qual parte é (
title) - Qual variação está ativa (
highlighted)
Especificidade nunca é problema. Tudo usa uma única classe. Sem aninhamento, sem IDs, sem seletores descendentes.
Onde BEM Dói
Os nomes ficam longos. Muito longos.
.navigation__dropdown-menu__item--active {}Isso é real. Acontece em projetos grandes. O HTML fica visualmente poluído e a legibilidade cai quando componentes são profundos.
Outro problema: BEM não diz onde colocar os arquivos. Não define arquitetura de pastas. É só uma convenção de nomes.
OOCSS: Object-Oriented CSS
Nicole Sullivan criou o OOCSS em 2009. A premissa central é tratar elementos visuais como objetos reutilizáveis, separando dois conceitos:
- Estrutura vs Skin (aparência)
- Container vs Content (conteúdo)
Princípio 1: Separar Estrutura de Skin
/* Estrutura - define layout e dimensões */
.media-object {
display: flex;
align-items: flex-start;
gap: 1rem;
}
.media-object__body {
flex: 1;
}
/* Skin - define aparência visual */
.skin-rounded {
border-radius: 8px;
}
.skin-shadow {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.skin-bordered {
border: 1px solid #e2e8f0;
}Você compõe objetos combinando classes:
<div class="media-object skin-rounded skin-shadow">
<img class="avatar skin-rounded" src="/user.jpg" alt="Avatar" />
<div class="media-object__body">
<h3>Nome do Usuário</h3>
<p>Comentário do usuário aqui.</p>
</div>
</div>Princípio 2: Separar Container de Content
O estilo de um elemento nunca deve depender de onde ele está. Isso é proibido:
/* ❌ Anti-pattern OOCSS */
.sidebar h3 {
font-size: 0.875rem;
color: #64748b;
}
.main-content h3 {
font-size: 1.5rem;
color: #1e293b;
}O correto é criar classes explícitas:
/* ✅ Padrão OOCSS */
.heading-sm {
font-size: 0.875rem;
color: #64748b;
}
.heading-lg {
font-size: 1.5rem;
color: #1e293b;
}Um Componente OOCSS Completo
/* Objeto base: botão */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.625rem 1.25rem;
font-weight: 600;
font-size: 0.875rem;
border: 2px solid transparent;
border-radius: 6px;
cursor: pointer;
transition: all 0.15s ease;
}
/* Skins de cor */
.btn-primary {
background: #3b82f6;
color: #fff;
}
.btn-primary:hover {
background: #2563eb;
}
.btn-ghost {
background: transparent;
color: #3b82f6;
border-color: #3b82f6;
}
.btn-ghost:hover {
background: #eff6ff;
}
/* Skins de tamanho */
.btn-sm {
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
}
.btn-lg {
padding: 0.875rem 1.75rem;
font-size: 1rem;
}<button class="btn btn-primary btn-lg">Salvar Projeto</button>
<button class="btn btn-ghost btn-sm">Cancelar</button>Onde OOCSS Brilha e Onde Falha
A reutilização é excelente. O CSS final tende a ser menor porque você evita duplicação. O Bootstrap inteiro foi construído sobre princípios OOCSS.
O problema? Não existe convenção de nomenclatura formal. Cada equipe inventa a sua. Sem disciplina, o projeto vira uma sopa de classes genéricas que ninguém sabe se pode alterar sem quebrar outra coisa.
Utility-First: O Paradigma do Tailwind
Utility-First leva o OOCSS ao extremo. Cada classe faz uma única coisa. Você compõe a UI inteiramente no HTML.
O Tailwind CSS é o representante dominante dessa abordagem.
Setup Real com Tailwind
npm install -D tailwindcss @tailwindcss/postcss postcssCrie o arquivo postcss.config.js:
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};No seu CSS principal:
@import "tailwindcss";O Mesmo Card, Agora em Utility-First
<article class="overflow-hidden rounded-lg border border-slate-200 bg-white">
<img
class="h-52 w-full object-cover"
src="/hero.jpg"
alt="Post destaque"
/>
<div class="p-6">
<h2 class="mb-2 text-xl font-bold text-slate-800">
Título do Post
</h2>
<p class="text-sm leading-relaxed text-slate-500">
Resumo breve do conteúdo...
</p>
<a
href="/post"
class="mt-4 inline-block font-semibold text-blue-500 hover:text-blue-700"
>
Leia mais
</a>
</div>
</article>Zero CSS customizado. Tudo declarado inline via classes utilitárias.
Variante Featured com Classes Condicionais
Em React, a composição fica natural:
interface CardProps {
title: string;
excerpt: string;
href: string;
featured?: boolean;
}
function Card({ title, excerpt, href, featured = false }: CardProps) {
return (
<article
className={`
overflow-hidden rounded-lg border bg-white
${featured
? "border-blue-500 shadow-lg shadow-blue-500/15"
: "border-slate-200"
}
`}
>
<div className="p-6">
<h2 className="mb-2 text-xl font-bold text-slate-800">
{title}
</h2>
<p className="text-sm leading-relaxed text-slate-500">
{excerpt}
</p>
<a
href={href}
className="mt-4 inline-block font-semibold text-blue-500
hover:text-blue-700 transition-colors"
>
Leia mais
</a>
</div>
</article>
);
}Extraindo Componentes com @apply
Quando a repetição incomoda, o Tailwind oferece @apply:
/* components.css */
.btn-primary {
@apply inline-flex items-center justify-center rounded-md
bg-blue-500 px-5 py-2.5 text-sm font-semibold text-white
transition-colors hover:bg-blue-600
focus:outline-none focus:ring-2 focus:ring-blue-500
focus:ring-offset-2;
}
.btn-ghost {
@apply inline-flex items-center justify-center rounded-md
border-2 border-blue-500 bg-transparent px-5 py-2.5
text-sm font-semibold text-blue-500
transition-colors hover:bg-blue-50;
}Use com moderação. Abusar do @apply é basicamente escrever CSS normal com passos extras.
Comparação Direta: O Mesmo Componente em 3 Metodologias
Vamos construir um alert banner nas três abordagens.
BEM
.alert {
padding: 1rem 1.25rem;
border-radius: 8px;
border-left: 4px solid;
font-size: 0.875rem;
line-height: 1.5;
}
.alert--success {
background: #f0fdf4;
border-color: #22c55e;
color: #166534;
}
.alert--error {
background: #fef2f2;
border-color: #ef4444;
color: #991b1b;
}
.alert__title {
font-weight: 700;
margin-bottom: 0.25rem;
}
.alert__message {
opacity: 0.9;
}<div class="alert alert--success">
<p class="alert__title">Sucesso!</p>
<p class="alert__message">Seu arquivo foi enviado.</p>
</div>OOCSS
.box {
padding: 1rem 1.25rem;
border-radius: 8px;
font-size: 0.875rem;
line-height: 1.5;
}
.box-left-accent {
border-left: 4px solid;
}
.theme-success {
background: #f0fdf4;
border-color: #22c55e;
color: #166534;
}
.theme-error {
background: #fef2f2;
border-color: #ef4444;
color: #991b1b;
}
.text-bold {
font-weight: 700;
}
.mb-xs {
margin-bottom: 0.25rem;
}<div class="box box-left-accent theme-success">
<p class="text-bold mb-xs">Sucesso!</p>
<p>Seu arquivo foi enviado.</p>
</div>Utility-First (Tailwind)
<div class="rounded-lg border-l-4 border-green-500 bg-green-50
px-5 py-4 text-sm leading-relaxed text-green-800">
<p class="mb-1 font-bold">Sucesso!</p>
<p class="opacity-90">Seu arquivo foi enviado.</p>
</div>Tabela de Trade-Offs
| Critério | BEM | OOCSS | Utility-First |
|---|---|---|---|
| Curva de aprendizado | Baixa | Média | Média-Alta |
| Legibilidade do HTML | Alta | Média | Baixa inicialmente |
| Legibilidade do CSS | Alta | Média | N/A (quase sem CSS) |
| Reutilização | Média | Alta | Muito Alta |
| Tamanho do CSS final | Grande | Médio | Pequeno (com purge) |
| Refatoração segura | Fácil | Arriscada | Muito Fácil |
| Onboarding de juniors | Fácil | Difícil | Médio |
Quando Usar Cada Uma
Escolha BEM quando:
- A equipe é grande e heterogênea em senioridade
- O projeto não usa framework de componentes
- Você precisa de CSS legível em code review
- O design system é estável e muda pouco
Escolha OOCSS quando:
- Você está construindo um framework CSS interno
- Precisa de máxima reutilização entre projetos
- A equipe tem maturidade para manter disciplina
Escolha Utility-First quando:
- O projeto usa React, Vue ou Svelte (componentes)
- Velocidade de prototipação é prioridade
- Você quer bundle CSS mínimo em produção
- A equipe aceita a verbosidade no HTML
Abordagem Híbrida: O Que Funciona na Prática
Projetos reais raramente usam uma metodologia pura. A combinação mais eficaz que já usei:
// Tailwind para layout e espaçamento
// Classes semânticas para componentes complexos
function Dashboard() {
return (
<div className="grid grid-cols-12 gap-6 p-8">
<aside className="col-span-3">
<Navigation />
</aside>
<main className="col-span-9">
<div className="mb-6 flex items-center justify-between">
<h1 className="text-2xl font-bold text-slate-900">
Dashboard
</h1>
<button className="btn-primary">Novo Projeto</button>
</div>
<StatsGrid />
</main>
</div>
);
}Layout com utilities. Componentes interativos complexos com classes extraídas. É pragmático.
Configurando o Melhor dos Dois Mundos
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ["./src/**/*.{ts,tsx,mdx}"],
theme: {
extend: {
colors: {
brand: {
50: "#eff6ff",
500: "#3b82f6",
700: "#1d4ed8",
900: "#1e3a5f",
},
},
spacing: {
18: "4.5rem",
88: "22rem",
},
},
},
};/* src/styles/components.css */
@import "tailwindcss";
/* Apenas componentes que aparecem 5+ vezes */
.btn-primary {
@apply inline-flex items-center justify-center rounded-md
bg-brand-500 px-5 py-2.5 text-sm font-semibold text-white
transition-colors hover:bg-brand-700
focus:ring-2 focus:ring-brand-500 focus:ring-offset-2;
}
.input-field {
@apply w-full rounded-md border border-slate-300 px-3 py-2
text-sm text-slate-900 placeholder:text-slate-400
focus:border-brand-500 focus:outline-none focus:ring-1
focus:ring-brand-500;
}A regra de ouro: só extraia com @apply o que aparece em cinco ou mais lugares. Antes disso, copie as utilities sem culpa.
Minha Opinião Sincera
Eu usei BEM por anos. É sólido. Funciona. Mas depois que migrei para Utility-First com Tailwind em projetos React, não voltei.
O motivo é simples: componentes já são a abstração.
Com BEM, você cria uma camada de nomes no CSS que espelha a estrutura do HTML. Em React, essa camada é redundante. O componente <Card> já encapsula markup e estilo. Nomear .card__title dentro de Card.tsx é burocracia sem benefício.
OOCSS tem mérito teórico, mas na prática é frágil. Sem uma convenção de nomes rígida como BEM, a "orientação a objetos" vira anarquia. Já vi projetos com 200 classes utilitárias caseiras que ninguém documentou. O Tailwind resolve isso ao ser o dicionário completo que sua equipe nunca vai escrever.
Minha crítica ao Tailwind? O HTML fica feio. Isso é real. Linhas com 15 classes assustam. Mas é um trade-off que aceito porque:
- Deletar componente = deletar estilos. Sem CSS órfão.
- O bundle final com purge é minúsculo. Frequentemente abaixo de 10KB.
- Refatorar é seguro. Não existe efeito colateral global.
Se você trabalha sem framework de componentes — sites WordPress, templates server-side puros — BEM ainda é rei. A clareza dos nomes compensa a verbosidade.
Se está construindo um design system para múltiplos produtos, OOCSS como filosofia (não como implementação) é valioso. Pense em tokens e primitivas.
Mas para a maioria dos projetos modernos com React, Next.js, Vue ou Svelte? Utility-First com Tailwind é a resposta pragmática. Não é a mais elegante. É a que menos faz você enlouquecer.
Pare de debater metodologias. Escolha uma, documente e siga. O CSS que mata projetos não é o mal escrito. É o que não segue padrão nenhum.
Leia tambem

Escrito por
Marcos Soares
Fullstack Developer · CEO da Agência Poti
Fullstack Developer e CEO da Agência Poti. Mais de 20 anos construindo arquiteturas cloud-native com React, Next.js e sistemas distribuídos. Parceiro comercial do estúdio iellou design. Fundador do Vivo de Código.
Comentários
Participe da discussão
Seja o primeiro a comentar!
Continue Aprofundando
Conteúdo técnico toda semana
Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.
Sem spam. Cancele a qualquer momento com 1 clique.


