Capa do Ebook gratuito Arquitetura de CSS Escalável: BEM, ITCSS e Design Tokens para Projetos Reais

Arquitetura de CSS Escalável: BEM, ITCSS e Design Tokens para Projetos Reais

Novo curso

22 páginas

Mapeamento de componentes para BEM: estados, temas, tamanhos e composição sem cascata frágil

Capítulo 7

Tempo estimado de leitura: 14 minutos

+ Exercício

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...
Download App

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 :disabled quando 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 cursor
  • card--selected: realce de seleção
  • card--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.

Agora responda o exercício sobre o conteúdo:

Ao aplicar BEM em componentes reais, qual abordagem reduz a cascata frágil ao lidar com estados, temas, tamanhos e composição?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Para evitar efeitos colaterais, estados/temas/tamanhos devem ficar explícitos no próprio componente via modificadores, enquanto o pai organiza apenas o layout. Assim o componente não muda de aparência por ter mudado de lugar, reduzindo dependência de seletores contextuais.

Próximo capitúlo

Padrões de objetos e layout reutilizáveis: grids, wrappers, media objects e alinhamentos

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.