Tudo o que você precisa saber sobre a arquitetura BEM CSS

Camilo Micheletto - Oct 1 - - Dev Community

Escrevi esse artigo visando responder perguntas sobre BEM que geralmente não são feitas, mas a ausência de suas respostas impactam no desenvolvimento e entendimento da arquitetura.

Esse artigo não é voltado pra pessoas iniciantes que estão conhecendo BEM CSS agora e/ou nunca tiveram contato com convenções de nomenclatura de classes CSS. Se esse é o seu caso, pule pra seção de fontes.

Header adaptado do site oficial legado do BEM CSS

 


🔗 Pra que serve uma arquitetura CSS?

Tal como as plantas, a forma que um código vai crescer e prosperar depende de como a gente cria, mantém e de onde as coisas estão localizadas.

Existem plantas com necessidades diferentes de solo, trato, água e luz do sol, tal como existem códigos com necessidades diferentes de padrões, localização e gerenciamento.

CSS é um elemento crítico no carregamento de uma página - o processo de renderização não inicia antes do navegador requisitar, baixar e transformar todo HTML e CSS em DOM e CSSOM e montar a árvore de render. Quanto menos CSS, melhor vai ser a performance, quanto mais organizado, padronizado e robusto o CSS, melhor ele é de manter, gerenciar, escalar e comprimir.

A forma de nomear os seletores CSS influencia a abrangência das regras, especificidade, localização e semântica desses, agilizando ou deteriorando o processo de desenvolvimento. Em projetos que mais de uma pessoa escreve CSS, temos diferentes níveis de habilidade na linguagem e diferentes costumes ou padrões pessoais dela, que se não gerenciados podem resultar em repetição excessiva de regras e bugs.


🔗 O que é o BEM CSS?

BEM é uma arquitetura CSS focada em styleguide, ela define padrões de categorização e escrita de regras CSS. O BEM foi criado na Yandex num contexto de aplicações que continham diversas páginas gerenciadas por uma ou poucas folhas de estilo.

BEM é a sigla pra Block, Element, Modifier. Cada uma dessas entidades representa uma categoria de elemento da interface, que é diretamente representada no CSS.

Representação de componentes de interface visualmente separados, Fonte: Site oficial do BEM CSS

 


🔗 Categorizando elementos da UI de acordo com seu acoplamento

Um Block representa um elemento de UI que é independente, ele pode ter filhos, como um header tem seus links de navegação e esses filhos, se forem independentes, podem ser blocos também.

O conceito de independência de um componente da interface pode ser definido pelo seguinte axioma

"Se o componente só faz sentido dentro de um contexto específico, ele deve ser um elemento daquele contexto. Se ele pode existir em diversos contextos, ele deve ser um bloco independente."

Um element é um componente que compõe outro maior e pertence a ele. Caso esse componente não exista e não dependa somente do contexto que ele está aplicado, ele pode ser um block. No BEM os blocks podem conter outros blocos, se um element precisar conter um element, ele provavelmente também é um bloco.

A relação entre um bloco e um elemento é representada pelo duplo underline block__element.

Blocos e elementos podem conter variações estéticas ou de layout dentro de um mesmo contexto. Pra isso temos modifiers. Essas classes podem representar diferentes estados ou variantes de um mesmo componente. Modificadores tal como elementos dependem do bloco e derivam apenas dele.

A relação entre um bloco ou um elemento e seus modificadores é representada pelo traço duplo block--modifier ou block__element--modifier.

 

Referência Descrição
Checkbox do bootstrap5 selecionado Um checkbox é independente, pode ser aplicado dentro de outros componentes como <form>, <div> ou <FieldContainer />, por exemplo.
Checkbox com label do bootstrap nas suas variantes checked e não checked Uma label pode ser considerada um bloco independente se ela for igualmente aplicada nos inputs da aplicação. Se a diferença entre labels for estética, ela pode ser um bloco que contém diversas variantes (modifiers), se a diferença entre as labels do input for estrutural (no template), faz sentido ela ser o elemento de um bloco
Componente de card do bootstrap 5 Um card pode ser incluído em qualquer container, em diferentes contextos, podendo ser considerado independente (block). Se a imagem e textos dentro dos cards tiverem características que só faz sentido no contexto do card, elas podem ser consideradas elements
Botões primary e secondary do bootstrap Um botão pode ser administrado em qualquer lugar, inclusive mais de uma variante no mesmo lugar. Cada variante pode ser representada pela classe modificadora derivada do mesmo bloco ou elemento.

 

🔗 Expressando a relação dos componentes através do CSS

Usando o exemplo do checkbox, a forma que construímos os componentes e definimos as suas responsabilidades influencia naquilo que pode ser bloco, elemento ou modificador. Essa tomada de decisão começa no template:

<div class="form-field">
  <input
   class="form-field__input form-field__input--checkbox" 
   type="checkbox"
   value=""
   id="checkbox"
  />
  <label class="form-field__label" for="checkbox">
    Default checkbox
  </label>
</div>
Enter fullscreen mode Exit fullscreen mode

Nesse template temos como bloco o componente .form-field que sempre vai conter um input .form-field__input e uma label de classe .form-field__label. A relação entre bloco (.form-field) e elemento (label ou input) é expressa por um underline duplo. Essa escrita faz com que através do CSS a gente entenda que:

  • form-field é uma entidade independente, ela não depende contexto do componente ou container pai pra seus estilos funcionarem, qualquer formulário pode receber campos do tipo .form-field

  • Por sua vez, o form-field pode conter um __input e uma __label, e seu layout e aparência depende do container form-field

.form-field {}
  .form-field__input {}
  .form-field__input--checkbox {}
  .form-field__label {}

Enter fullscreen mode Exit fullscreen mode

 

Ao alterarmos a relação no template, alteramos a topografia da classe CSS. No exemplo abaixo input e label são entidades separadas:

<div class="column">
  <label class="label" for="checkbox">
    Default checkbox
  </label>
  <input
   class="input input--checkbox"
   type="checkbox"
   value=""
   id="checkbox"
  />
</div>
Enter fullscreen mode Exit fullscreen mode
  • .column nesse caso pode ser vista tanto como um bloco como simplesmente uma classe utilitária. Além de outras arquiteturas CSS, é possível usar BEM com classes utilitárias, inclusive com essas seguindo sua própria convenção.
  • .input é um bloco que se refere à todos os inputs, podendo ser utilizado dentro de um container .column ou qualquer outro, funcionando independente da existência de .label. Possúi uma variante --checkbox que carrega estilos específicos pro input do tipo checkbox, como o estado de :checked.
  • .label é um bloco que seu layout e aparência não dependem do input, podendo até ter um .input como elemento filho.

Expresso em CSS, ficaria da seguinte forma:

.column {}

.input {}
.input--checkbox {}
.input--checkbox:checked {}

.label {}

Enter fullscreen mode Exit fullscreen mode

 

O BEM foca a independência dos componentes - alterar um componente de lugar no HTML não deve afetar seu layout, alterar o CSS de um componente não deve afetar os outros componentes. Quando criamos elementos, demonstramos uma relação daquilo que será afetado quando alteramos seu bloco. Dessa forma você tem visibilidade do que seria um componente acoplado com suas partes e um módulo completamente independente. Como citado na documentação legada:

In 2006, we started working on our first large projects: Yandex.Music and Ya.Ru. These projects, with dozens of pages, revealed the main drawbacks of the current approach to development:

  • Any changes to the code of one page affected the code of other pages.
  • It was difficult to choose classnames.

Tradução

Em 2006, começamos a trabalhar no nosso primeiro projeto grande: Yandex.Music e o Ya.Ru. Esses projetos que tinham dúzias de páginas se revelavam as desvantagens da atual abordagem de desenvolvimento:

  • Qualquer mudança no código de uma página afetaria as outras páginas.
  • Era difícil escolher como nomear as classes.

 

🔗 Relações de componentes complexos

Em 2010 quando o BEM se tornou open source e ganhou um site a ideia de que cada elemento de UI era um 'componente' já se fazia presente, mesmo antes do atomic design (Brad Frost, 2013) em resposta ao desafio de manter a consistência de componentes iguais em páginas diferentes. Na época da criação do BEM na Yandex, por exemplo, usavam vários arquivos HTML e apenas um arquivo de CSS e um de Javascript.

Se cada página fosse um bloco e seus elementos fossem dependentes do contexto dessa página, todo o código que fosse consistente entre páginas seria duplicado. Separar a UI em pequenos componentes permitia o reuso dessas partes independente de que páginas elas fossem utilizadas.

Pra exemplificar esse conceito de reuso, podemos usar um componente mais complexo, como de um header:

Componente header do bootstrap 5

Esse header pode ser escrito da seguinte forma (HTML resumido):

<nav class="navbar">
  <div class="navbar__container">
    <a class="navbar__logo" href="#">Navbar</a>
    <button class="navbar__toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar__toggler-icon"></span>
    </button>
    <div class="navbar__collapse" id="navbarSupportedContent">
      <ul class="navbar__list">
        <li class="navbar__item">
          <a class="navbar__link active" aria-current="page" href="#">Home</a>
        </li>
        <!-- etc -->
        <li class="navbar__item navbar__dropdown">
          <a class="navbar__dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
            Dropdown
          </a>
          <ul class="navbar__dropdown-menu">
            <li>
              <a class="navbar__dropdown-item" href="#">
                Action
              </a>
            </li>
            <!-- etc -->
          </ul>
        </li>
        <!-- etc -->
      </ul>
      <form class="navbar__form" role="search">
        <input class="navbar__form-input" type="search" placeholder="Search" aria-label="Search">
        <button class="navbar__form-btn" type="submit">
          Search
        </button>
      </form>
    </div>
  </div>
</nav>

Enter fullscreen mode Exit fullscreen mode

Perceba que nesse exemplo todo o navbar foi declarado como um grande bloco e tudo o que ele continha foi considerado element de navbar. Com isso, perdemos diversas oportunidades de reuso.

Oportunidade Descrição
Logo do navbar representado por uma imagem ou texto dentro de um link O logotipo dentro de .navbar__logo é representado por um link contendo imagem ou texto. Existem diversos elementos que podem ser 'envelopados' em um link, como ícones back to top ou cards clicáveis. Esse código poderia pertencer a um bloco .link ou .media
Lista de itens do navbar do bootstrap 5 Os itens .navbar__list e .navbar__item nada mais são que uma lista desordenada no formato de linha ao invés de coluna. Isso poderia reutilizado representando-a pelo bloco .list com o modificador .list--row. O dropdown por sua vez pode ter o mesmo funcionamento fora do navbar, dentro do seu próprio bloco .dropdown, com seus elementos .dropdown__toggle e, como ele renderiza uma lista, podemos reutilizar .list e, se necessário, complementar com um modificador
Input de texto com botão de busca do boostrap 5 Esse componente de busca pode ser representado de forma mais "fechada" com um bloco .search e elementos .search__input, search__label e .search__trigger ou de forma mais "aberta" como .form-field, .input--text, .label e .button--outline

 

Ainda pensando nessa relação de componentes "abertos" e "fechados", podemos representar o componente de busca usado no último exemplo das duas forma usando CSS.

Componente de busca "fechado"
Os elementos input, label e button são acoplados ao bloco search, mudanças no componente ou no HTML de search influenciam nos seus elementos.

.search {}
  .search__input {}
  .search__label {}
  .search__button {}
Enter fullscreen mode Exit fullscreen mode

Componente de busca "aberto"
Os elementos input, label e button são independentes do bloco de search, ele apenas organiza os blocos já existentes que compõe esse componente.

.form-field {}

.input {}
.input--text {}

.label {}

.button {}
.button--outline {}
Enter fullscreen mode Exit fullscreen mode

🔗 Controle da especificidade com BEM

Um dos problemas citados pela galera do Yandex é que mudanças no CSS podiam afetar partes indesejadas. No caso deles era utilizada uma folha de estilo pra todas as páginas da aplicação, e você pode achar que não é um problema pra ti se você usa algum processador como Sass ou diversos arquivos de CSS, mas é uma dor de cabeça ainda maior quando seletores de diferentes abrangências moram em múltiplos arquivos.

A forma que o BEM encontrou de contornar esse problema além da modularização de blocos é a especificidade. BEM só utiliza classes por motivos bem claros:

  • Classes podem ser reaproveitadas
  • Como você se refere diretamente aos elementos usando as classes, a especificidade tende a ser sempre muito próxima de 0,1,0
  • Pra alterar ou sobrescrever uma regra, basta adicionar outra classe
  • Regras que se referem à seletores mais globais como de resets ou normalizers são facilmente sobrescritas pelas classes.

Abaixo vemos um accordion que mantém uma diversidade de seletores, pra nos referirmos, por exemplo ao h2 desse accordion, temos que explicitamente dizer #accordion-item > h2:

<div id="accordion-item">
  <h2>
    <button class="btn">
      Accordion Item #1
    </button>
  </h2>
  <div class="collapse show">
    <div>
      <strong>This is the first item's accordion body.</strong>
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

 

Pra estilizar todos os elementos desse componente, precisamos estabelecer as seguintes relações:

/* 1,0,0 */
#accordion-item {}

/* 1,0,1 */
#accordion-item > h2 {}

/* 1,1,0 */
#accordion-item .btn {}

/* 1,1,0 */
#accordion-item .collapse {}

/* 1,2,0 */
#accordion-item > .collapse.show {}

/* 1,1,1 */
#accordion-item > .collapse > div {}

/* 1,1,2 */
#accordion-item > .collapse strong {}

Enter fullscreen mode Exit fullscreen mode

Esse exemplo assume que .collapse por não se referir diretamente ao accordion (não é um .accordion-collapse, por exemplo) pode existir em outros lugares do código ou até em outros lugares de #accordion-item. Isso obriga a gente a deixar explícita sua relação com #accordion-item.

A regra #accordion-item > .collapse > div existe pois o estilo não pode impactar qualquer div, nem qualquer .collapse, pra alterar apenas aquilo que existe dentro do bloco #accordion-item precisamos adicionar muito mais complexidade e especificidade, além de que qualquer mudança no HTML desse componente causa bugs imprevisíveis.

<div class="accordion-item">
  <h2 class="title">
    <button class="button">
      Accordion Item #1
    </button>
  </h2>
  <div class="accordion-item__collapse">
    <div class="accordion-item__wrapper">
      <strong>This is the first item's accordion body.</strong>
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

No HTML acima estabelecemos as relações entre o .accordion-item e o .accordion-item__collapse sem adicionar especificidade. Ao nos referirmos diretamente aos elementos pela classe e não compondo seletores, quando alteramos o .accordion-item podemos quebrar o .accordion-item__collapse, mas ao alterar .accordion-item__collapse, o .accordion-item dificilmente será influenciado.

.title e .button serem blocos independentes faz com que alterarmos o HTML desse componente ou o CSS dessas classes não cause nenhum bug, pois elas existem de forma independente do bloco do accordion. Em relação à especificidade, a relação passa a ser da seguinte forma:

/* 0,1,0 */
.accordion-item {}

/* 0,1,0 */
.accordion-item__collapse {}

/* 0,1,0 */
.accordion-item__wrapper {}

/* 0,1,0 */
.title {}

/* 0,1,0 */
.button {}

Enter fullscreen mode Exit fullscreen mode

 

A especificidade dessas regras é muito mais previsível, tanto que se eu quiser adicionar uma variante ao .accordion-item__wrapper basta eu criar uma classe com as regras e incluir no elemento no HTML, sem precisar criar no CSS um seletor .accordion-item__wrapper.accordion-item__wrapper--vertical.


🔗 Reuso de CSS

Quando me refiro a reuso de CSS, não falo apenas de blocos que podem ser reaplicados em diferentes componentes, à nível de seletores, mas também à conjuntos de regras que realizam as mesmas ações. No exemplo abaixo, temos 3 componentes diferentes e mesmo se escritos na convenção do BEM haverá repetição de conjuntos de regras:

Elemento Regras
Alert do bootstrap 5

.alert {
  display: flex;
  gap: 0.5rem;
  /* etc... */
}
Nav/tab do bootstrap 5

.nav {
  display: flex;
  gap: 1rem;
  /* etc... */
}
Breadcrumb do bootratrap 5

.breadcrumb {
  display: flex;
  gap: 0.5rem;
  /* etc... */
}

 

Percebe como todos os elementos que tem a disposição de 'linha' muito provavelmente terão um grupo de regras de display: flex o configura dessa forma, terão um gap: npx e provavelmente um flex-wrap: wrap quando fizer sentido pro layout 'quebrar' pra linha de baixo?

Usando o breadcrumb como exemplo, podemos criar um bloco que represente uma linha ou coluna genérica e que seja configurável à partir de variantes.

<ol class="breadcrumb">
  <li class="breadcrumb__item">Home</li>
  <li class="breadcrumb__item active">Library</li>
</ol>
Enter fullscreen mode Exit fullscreen mode

Ao invés de repetir o conjunto de regras que configura uma coluna, podemos adicionar à classe .breadcrumb o bloco de .row. Não há restrições sobre o mesmo elemento ser representado por dois blocos distintos, pois o conceito de bloco é acoplado com seus elementos, não com componentes específicos.

/* Breadcrumb deixa de configurar o layout e passa apenas a implementar estilos estéticos */
.breadcrumb {
  font: 200 1rem Poppins;
  line-height: 1.45;
}

/* Row se torna responsável pelo layout */
.row {
  display: flex;
  gap: 1rem;
}

/* E pode ter variantes na convenção BEM caso conveniente */
.row--flexible {
  flex-wrap: wrap;
}

Enter fullscreen mode Exit fullscreen mode

Nesse caso o gap de .row é estático, mas os elementos apresentados nos exemplos tem diferentes valores nessa propriedade, de quem é a responsabilidade? Se você possuí uma estrutura de CSS utilitário, a responsabilidade pode ser de .row que será configurada pelo CSS utilitário .gap-0.5 ou .gap-sm.

Se você não usa CSS utilitário, o gap pode ser configurado pelo próprio componente breadcrumb:


/* Ao implementar o gap com variáveis CSS temos um valor
default e um valor configurável */
.row {
  --row__gap-default: 1rem;
  display: flex;
  gap: var(--row__gap, var(--row__gap-default));
}

/* Dessa forma podemos configurar o gap pelo breadcrumb.
A row ainda não depende dele, mas caso ele seja aplicado com
uma row, ele a configura */
.breadcrumb {
  --row__gap: 0.5rem;
  font: 200 1rem Poppins;
  line-height: 1.45;
}

Enter fullscreen mode Exit fullscreen mode

Separar os estilos de layout dos de aparência é uma prática do OOCSS (link do artigo na Smashing Magazine, em inglês). Categorizar o CSS em skin e structure vem do entendimento de que a estrutura e layout de um elemento ou componente é mais reaproveitável e ubíqua do que a aparência de um componente.

Usando a convenção BEM é possível criar blocos que se referem à estruturas e modificadores que se referem à aparência, sendo ela apenas uma convenção de escrita de classes, a categorização e separação dessas classes pode ficar na responsabilidade de outro tipo de arquitetura.


🔗 BEM e Sass

Sass foi criado em 2009, um ano antes de BEM se tornar open source, a interação entre eles funciona extremamente bem (rsrs) por dois motivos:

  • Sass permite a criação de múltiplas folhas de estilo e a co-locação dos estilos de acordo com as necessidades do projeto,logo podemos criar uma folha pra cada bloco ou conjunto de blocos que se referem a um componente ou contexto.
  • Sass tem suporte à concatenação de string, o que torna ainda mais visual a relação da classe com seus elementos e modificadores. Pensando no exemplo do bloco .search citado anteriormente:
.search {
  &__input {}
  &__label {}
  &__button {}
}
Enter fullscreen mode Exit fullscreen mode

 

Com CSS a relação de blocos e elementos se dá pelo prefixo do bloco, ex: .search__, no Sass os elementos são declarados dentro do bloco.

Um problema muito comum com Sass é o 'overnesting', que é quando aninhamos múltiplas classes em um bloco. Essa abordagem além de dificultar a legibilidade do bloco cria especificidade sem necessidade e acopla os elementos ao bloco mais do que o necessário.

/* Esse tipo de nesting ao invés de gerar seletores  0,1,0 */
.search {
  .search__item {}

  .search__label {
    &.search__label--floating {

    }
  }
}

/* Gera o CSS */
/* 0.2.0 */
.search .search__item {}

/* 0.2.0 */
.search .search__label {}

/* 0.3.0 */
.search .search__label.search__label--floating {}
Enter fullscreen mode Exit fullscreen mode

O overnesting geralmente acontece pelo não conhecimento de como o Sass funciona ou pela tentativa de imitar o formato do HTML no CSS.


🔗 Interações com outras arquiteturas

Como naming convention BEM é flexível quando nos apropriamos de outras arquiteturas ou apenas características delas. Como citado na documentação:

No matter what methodology you choose to use in your projects, you will benefit from the advantages of more structured CSS and UI. Some styles are less strict and more flexible, while others are easier to understand and adapt in a team.

Tradução

Não importa qual metodologia você use em seus projetos, vbocê vai se beneficiar das vantagens de uma UI e CSS mais estruturados. Alguns estilos são menos estritos e mais flexíveis, outros sãi fáceis de entender e adaptar em um time.

 

Podemos escrever as regras do tipo block do CUBECSS na convenção do BEM.

A diferença entre os blocos do CUBECSS e do BEM é o seu escopo, no CUBE há uma separação semântica entre estrutura, configuração e aparência. Após declarar a camada de Composition que defini layouts num nível mais macro (Como o object no OOCSS ou ITCSS) e criar classes utilitárias na camada de Utility, sobra pouca configuração a se fazer na cabada do Block, o deixando mais enxuto.

Podemos também criar as diversas camadas propostas pelo ITCSS e criar Objects, Components e Utilities na sintaxe do BEM. O ITCSS não define a forma que criamos modificadores e a relação dos componentes e seus estados/variantes, mas sim a taxonomia e localização dos elementos respeitando seu escopo e dando previsibilidade na sua especificidade e posição na cascata.

Podemos expressar as mesmas relações do SMACSS, por exemplo, declarando states quando esses alteram um módulo específico na sintaxe BEM:

/* Na convenção SMACSS */
.tab {
  background-color: purple;
  color: white;
}

.is-tab-active {
  background-color: white;
  color: black;
}

/* Na convenção BEM */
.tab {
  background-color: purple;
  color: white;
  &__is-active {
    background-color: white;
    color: black;
  }
}
Enter fullscreen mode Exit fullscreen mode

States que não são específicos de um módulo podem ser declarados como classes utilitárias ou blocos independentes.

Há muitas outras formas de se escrever BEM, não necessariamente vinculadas a uma arquitetura, como no exemplo abaixo.


Afinal, BEM é a melhor arquitetura de CSS?

Não existe 'melhor arquitetura'. Todas as metodologias já propostas são claras nos problemas que elas visam resolver, e elas não necessariamente vão resolver todos os problemas da sua organização, time ou codebase. BEM pode resolver seus problemas de acoplamento de classes e especificidade, mas talvez não resolva o de performance ou localização.

O interessante é aumentar seu gabarito de metodologias, convenções e arquiteturas e escolher sua ferramenta de acordo com os problemas que tu visa resolver.

BEM num contexto de aplicação Vue com SFC pode ser incrível quando localizamos o bloco CSS no mesmo lugar que o componente que ele estiliza, mas pode gerar muito CSS duplicado ou inutilizado se não houver uma arquitetura mais voltada pra reuso de estruturas, como OOCSS ou CUBECSS.


🔗 Fontes

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player