Mapeando componentes reais para BEM sem criar cascata frágil
Quando você aplica BEM em um projeto real, o desafio não é “inventar nomes”, e sim mapear decisões de UI (estado, tema, tamanho, composição) para classes que não dependam de contexto, não exijam seletores longos e não criem efeitos colaterais. O objetivo deste capítulo é mostrar como transformar um componente de design (ex.: botão, card, campo de formulário) em um conjunto de classes BEM que seja previsível, extensível e resistente a mudanças de layout.
O mapeamento começa com uma pergunta simples: “O que é identidade do componente (bloco), o que é parte interna (elemento), e o que é variação (modificador)?” A partir disso, você decide como representar estados (disabled, loading, open), temas (brand, neutral, danger), tamanhos (sm, md, lg) e composição (um componente dentro do outro) sem depender de seletores do tipo .sidebar .button ou .card > .title para funcionar.
Estados em BEM: o que muda no comportamento vs. o que muda na aparência
Estados são condições temporárias ou situacionais do componente: “está carregando”, “está aberto”, “está selecionado”, “está inválido”. Em BEM, estados normalmente viram modificadores do bloco (ou do elemento) quando alteram a aparência/estrutura do próprio componente.
Regra prática: estado que pertence ao componente vira modificador do bloco
Se o estado descreve o componente como um todo, use .bloco--estado. Exemplos comuns:
.button--loading.dropdown--open.modal--visible.tabs--dense(quando “dense” é um modo/estado de exibição)
Isso evita depender de atributos ou seletores contextuais para “achar” o componente em determinado estado.
Continue em nosso aplicativo
Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.
ou continue lendo abaixo...Baixar o aplicativo
Quando o estado é de um elemento interno
Se o estado está ligado a uma parte específica, use modificador do elemento: .bloco__elemento--estado. Exemplo: item ativo em uma lista de tabs.
.tabs__tab { /* estilo base */ } .tabs__tab--active { /* tab ativa */ }Evite criar seletores como .tabs__tab.is-active se você quer manter uma convenção única. Se o projeto já usa classes utilitárias de estado (.is-active, .is-open), defina uma regra: ou elas são utilitárias globais (fora do BEM) ou você padroniza tudo em BEM. Misturar sem critério costuma gerar duplicidade e divergência de comportamento.
Estados “sem estilo” vs. estados “com estilo”
Alguns estados existem apenas para lógica (ex.: hooks de JS) e não deveriam carregar estilo. Nesses casos, você pode usar atributos ([data-state="loading"]) ou classes de hook (js-*) separadas do BEM. A regra é: se a classe existe para estilo, ela deve ser previsível e parte do contrato do componente; se existe para comportamento, não deve competir com o contrato de estilo.
<button class="button button--primary" data-state="loading">Salvar</button>Se o estado altera visual, você pode espelhar em BEM:
<button class="button button--primary button--loading">Salvar</button>Temas em BEM: variação sem “skin” acoplada ao layout
“Tema” aqui significa variação de aparência com significado (brand, danger, success) ou com contexto visual (light, dark, inverse). O risco comum é criar temas que dependem do contêiner, por exemplo: .sidebar .button { ... }. Isso cria cascata frágil: ao mover o botão para outro lugar, ele muda de aparência sem intenção.
Regra prática: tema é modificador do bloco, não do contêiner
Se um botão precisa ser “danger”, isso deve estar explícito no próprio botão:
<button class="button button--danger">Excluir</button>Assim, o componente carrega sua intenção. Se o tema é global (ex.: aplicação em modo escuro), prefira tokens e variáveis CSS no escopo do tema, sem reescrever seletores de componentes com alta especificidade.
Temas globais com escopo controlado (sem reescrever componentes)
Uma abordagem robusta é colocar o tema em um contêiner raiz e alterar apenas tokens (variáveis), deixando os componentes consumirem esses tokens. Exemplo:
:root { --color-surface: #ffffff; --color-text: #111111; --color-primary: #2f6fed; } .theme--dark { --color-surface: #0f1115; --color-text: #f2f4f8; --color-primary: #7aa2ff; } .card { background: var(--color-surface); color: var(--color-text); } .button--primary { background: var(--color-primary); color: var(--color-surface); }Note que o tema muda valores, não a estrutura do CSS do componente. Isso reduz a necessidade de “sobrescrever” regras e evita brigas de especificidade.
Quando tema é “variante do componente” e não “tema global”
Às vezes “tema” significa uma variante do componente (ex.: button--ghost, button--outline). Trate como modificador do bloco. O cuidado é não multiplicar combinações sem controle. Em vez de criar dezenas de variantes, defina um conjunto pequeno e coerente de modificadores que cubra o design system.
Tamanhos em BEM: escalas previsíveis e combináveis
Tamanho é uma variação típica: sm, md, lg. O erro comum é codificar tamanho em elementos internos com seletores contextuais (ex.: .button--sm .button__icon com muitas regras espalhadas). Isso pode ser necessário, mas deve ser feito com disciplina para não virar uma cascata de exceções.
Regra prática: tamanho é modificador do bloco
.button { padding: 0.75rem 1rem; font-size: 1rem; } .button--sm { padding: 0.5rem 0.75rem; font-size: 0.875rem; } .button--lg { padding: 1rem 1.25rem; font-size: 1.125rem; }Se elementos internos precisam ajustar com o tamanho (ícone, spinner), faça isso dentro do próprio arquivo do componente, com seletores curtos e previsíveis:
.button__icon { width: 1em; height: 1em; } .button--sm .button__icon { width: 0.9em; height: 0.9em; } .button--lg .button__icon { width: 1.1em; height: 1.1em; }Isso ainda é cascata, mas não é frágil porque depende apenas do próprio bloco e seus elementos, não de um contêiner externo desconhecido.
Evite “tamanho por contexto”
Evite regras como .toolbar .button { font-size: ... } para “encaixar” no layout. Se a toolbar exige botões menores, a toolbar deve declarar isso explicitamente no markup, por exemplo aplicando um modificador no botão (button--sm) ou usando uma variante de componente (ex.: toolbar__action button button--sm).
Composição sem cascata frágil: componentes dentro de componentes
Composição é quando um componente é usado dentro de outro (um button dentro de um card, um badge dentro de um table). A cascata frágil aparece quando o componente pai “alcança” o filho com seletores que alteram seu estilo base, criando dependência de contexto.
Princípio: o componente não deve mudar porque mudou de lugar
Se um .button muda de aparência ao ser colocado dentro de .card, você perde previsibilidade. Em vez disso, o pai deve controlar layout (espaçamento, alinhamento) e o filho controla sua aparência.
Exemplo: o card quer alinhar ações no rodapé. Em vez de:
.card .button { margin-left: 0.5rem; }Use um elemento do card para layout:
.card__actions { display: flex; gap: 0.5rem; justify-content: flex-end; }<div class="card"> <div class="card__content">...</div> <div class="card__actions"> <button class="button button--secondary">Cancelar</button> <button class="button button--primary">Salvar</button> </div></div>O card organiza; o botão mantém seu contrato visual.
Quando o pai precisa influenciar o filho: use “slots” e elementos do pai
Há casos em que o pai precisa impor restrições (ex.: um item de lista precisa que o badge seja menor). Em vez de sobrescrever o .badge diretamente, crie um “slot” no pai e aplique regras no slot, não no componente filho.
.listItem__meta { display: flex; align-items: center; gap: 0.25rem; } .listItem__meta .badge { /* evite */ }O exemplo acima ainda “alcança” o filho. Uma alternativa mais robusta é expor uma variante do badge para esse uso (ex.: badge--sm) e o pai apenas decide qual variante usar no markup:
<span class="badge badge--sm">Novo</span>Se você não controla o markup (ex.: conteúdo vindo de CMS), então o slot pode ser aceitável, mas trate como exceção documentada e mantenha o seletor o mais simples possível.
Composição com “wrapper” BEM para layout
Outra técnica é criar um wrapper do pai para controlar espaçamento sem tocar no filho. Exemplo: em vez de mudar o .input dentro de um formulário, você cria um elemento form__field que define margens, largura e grid.
.form__field { display: grid; gap: 0.25rem; margin-bottom: 1rem; } .form__label { font-weight: 600; } .form__hint { font-size: 0.875rem; color: var(--color-text-muted); }<div class="form__field"> <label class="form__label">Email</label> <input class="input input--md" /> <p class="form__hint">Use seu email corporativo.</p></div>O campo .input não precisa saber que está dentro de um formulário específico; o layout é responsabilidade do wrapper.
Passo a passo: mapeando um componente “Button” com estados, temas e tamanhos
Vamos mapear um botão típico que tem: ícone opcional, estado de loading, estado disabled, variantes de tema (primary, secondary, danger) e tamanhos (sm, md, lg). O objetivo é manter o CSS previsível e evitar seletores dependentes de contexto.
1) Defina o bloco e os elementos
Bloco: button. Elementos internos: button__icon, button__label, button__spinner.
<button class="button button--primary button--md" type="button"> <span class="button__icon" aria-hidden="true">...</span> <span class="button__label">Salvar</span></button>2) Liste modificadores por categoria (tema, tamanho, estado)
- Tema:
button--primary,button--secondary,button--danger - Tamanho:
button--sm,button--md,button--lg - Estado:
button--loading,button--disabled(ou usar:disabledquando for botão nativo)
Evite criar modificadores que misturam categorias, como button--primary-lg. Isso explode combinações e dificulta manutenção.
3) Escreva a base do bloco com tokens/variáveis e sem depender de contexto
.button { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; border: 1px solid transparent; border-radius: 0.5rem; font-weight: 600; cursor: pointer; user-select: none; } .button__icon { width: 1em; height: 1em; display: inline-flex; } .button__spinner { width: 1em; height: 1em; display: none; } .button__label { line-height: 1; }4) Aplique tamanhos como modificadores do bloco
.button--sm { padding: 0.5rem 0.75rem; font-size: 0.875rem; } .button--md { padding: 0.75rem 1rem; font-size: 1rem; } .button--lg { padding: 1rem 1.25rem; font-size: 1.125rem; }5) Aplique temas como modificadores do bloco
.button--primary { background: var(--color-primary); color: var(--color-on-primary); } .button--secondary { background: var(--color-surface); color: var(--color-text); border-color: var(--color-border); } .button--danger { background: var(--color-danger); color: var(--color-on-danger); }Perceba que o tema não depende de onde o botão está. Se o tema global mudar (ex.: dark), os tokens mudam e o botão acompanha.
6) Modele estados sem quebrar o layout
Loading normalmente precisa: desabilitar clique, esconder label (ou reduzir opacidade) e mostrar spinner. Faça isso com um modificador do bloco e regras internas controladas.
.button--loading { cursor: progress; } .button--loading .button__spinner { display: inline-flex; } .button--loading .button__icon { display: none; } .button--loading .button__label { opacity: 0.7; }Para disabled, se for <button> nativo, prefira :disabled para refletir acessibilidade e comportamento. Você pode combinar com um modificador se precisar suportar elementos não nativos (ex.: <a>).
.button:disabled, .button--disabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; }7) Defina regras para combinações permitidas
Nem toda combinação faz sentido. Por exemplo, button--loading deve funcionar com qualquer tema e tamanho. Então, escreva o CSS de loading sem assumir cores específicas. Se o spinner precisar de cor, use currentColor:
.button__spinner { color: currentColor; }Assim, o spinner herda a cor do texto do tema atual.
Passo a passo: mapeando um “Card” com composição e variações seguras
Agora um componente que costuma sofrer com cascata frágil: card com header, body, footer e possibilidade de “selecionado”, “clicável” e “denso”.
1) Defina bloco e elementos
<article class="card card--interactive"> <header class="card__header"> <h3 class="card__title">Plano Pro</h3> <p class="card__subtitle">Para equipes</p> </header> <div class="card__content">...</div> <footer class="card__actions"> <button class="button button--secondary button--sm">Detalhes</button> <button class="button button--primary button--sm">Assinar</button> </footer></article>2) Variações do card como modificadores do bloco
card--interactive: tem hover/foco e cursorcard--selected: realce de seleçãocard--dense: menos padding
.card { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: 0.75rem; padding: 1rem; } .card__header { display: grid; gap: 0.25rem; margin-bottom: 0.75rem; } .card__actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 1rem; } .card--dense { padding: 0.75rem; } .card--interactive { cursor: pointer; } .card--interactive:hover { border-color: var(--color-border-strong); } .card--selected { border-color: var(--color-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent); }O card não altera o botão. Ele apenas organiza o espaço onde o botão vive (card__actions). Isso é composição sem cascata frágil.
Checklist de decisões para evitar cascata frágil ao mapear BEM
1) O componente precisa “funcionar” sem o pai
Teste mental: se eu mover o componente para outro lugar, ele mantém aparência e comportamento? Se não, você provavelmente está usando regras contextuais demais.
2) Layout do pai, aparência do filho
Pai controla grid, gap, alinhamento, ordem. Filho controla cores, tipografia interna, bordas e estados visuais. Quando o pai começa a redefinir cores e padding do filho, a dependência cresce.
3) Modificadores por categoria, sem combinações explosivas
Separe tema, tamanho e estado em modificadores independentes. Isso permite combinações previsíveis e reduz a necessidade de criar variantes específicas para cada caso.
4) Estados devem ser explícitos e localizados
Se o estado altera visual, ele deve estar representado na classe do próprio bloco/elemento. Evite depender de “um pai em tal página” para ativar estado.
5) Exceções documentadas: quando usar seletor do pai para o filho
Se você precisar ajustar um componente dentro de um contexto específico e não controla o markup (ou o custo de criar uma variante é alto), mantenha o seletor curto e restrito ao “slot” do pai. Exemplo aceitável: .modal__footer .button apenas para espaçamento, não para redefinir tema/tamanho do botão.
.modal__footer { display: flex; justify-content: flex-end; gap: 0.5rem; } .modal__footer .button { /* apenas se inevitável; prefira gap no footer */ }Exemplo integrado: componente “Input” com estado inválido, tamanhos e composição com “Field”
Campos de formulário são um ótimo caso para separar responsabilidades: o input (controle) e o field (com label, hint e mensagem de erro). Em vez de o input tentar estilizar label/erro, crie um bloco de composição que organiza tudo.
Markup sugerido
<div class="field field--invalid"> <label class="field__label" for="email">Email</label> <input id="email" class="input input--md" type="email" /> <p class="field__message">Informe um email válido.</p></div>CSS do input (controle reutilizável)
.input { width: 100%; border: 1px solid var(--color-border); border-radius: 0.5rem; background: var(--color-surface); color: var(--color-text); } .input--sm { padding: 0.5rem 0.75rem; font-size: 0.875rem; } .input--md { padding: 0.75rem 0.875rem; font-size: 1rem; } .input--lg { padding: 0.875rem 1rem; font-size: 1.125rem; } .input:focus { outline: none; border-color: var(--color-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent); }CSS do field (composição e estados do conjunto)
.field { display: grid; gap: 0.375rem; } .field__label { font-weight: 600; } .field__message { font-size: 0.875rem; color: var(--color-text-muted); } .field--invalid .field__message { color: var(--color-danger); } .field--invalid .input { border-color: var(--color-danger); }Observe que a única “influência” do field sobre o input é para refletir um estado do conjunto (inválido). Isso é uma exceção justificável porque o estado pertence ao agrupamento (field) e precisa afetar o controle. Ainda assim, o seletor é simples e local (.field--invalid .input), não depende de páginas ou layouts externos.