Domando a cascata com seletores BEM e CSS modernos PlatoBlockchain Data Intelligence. Pesquisa vertical. Ai.

Domando a cascata com BEM e seletores CSS modernos

BEM. Como aparentemente todas as técnicas no mundo do desenvolvimento front-end, escrevendo CSS em um formato BEM pode ser polarizador. Mas é – pelo menos na minha bolha do Twitter – uma das metodologias CSS mais apreciadas.

Pessoalmente, acho que o BEM é bom e acho que você deveria usá-lo. Mas também entendo por que você pode não.

Independentemente da sua opinião sobre o BEM, ele oferece vários benefícios, sendo o maior o fato de ajudar a evitar conflitos de especificidade no CSS Cascade. Isso porque, se usado corretamente, qualquer seletor escrito em um formato BEM deve ter a mesma pontuação de especificidade (0,1,0). Eu projetei o CSS para muitos sites de grande escala ao longo dos anos (pense em governos, universidades e bancos), e é nesses projetos maiores que descobri que o BEM realmente brilha. Escrever CSS é muito mais divertido quando você tem certeza de que os estilos que está escrevendo ou editando não estão afetando alguma outra parte do site.

Na verdade, existem exceções em que é considerado totalmente aceitável adicionar especificidade. Por exemplo: o :hover e :focus pseudoclasses. Aqueles têm uma pontuação de especificidade de 0,2,0. Outra é pseudo elementos - como ::before e ::after - que têm uma pontuação de especificidade de 0,1,1. Para o restante deste artigo, porém, vamos supor que não queremos nenhum outro rastejamento de especificidade. 🤓

Mas não estou aqui para vender o BEM. Em vez disso, quero falar sobre como podemos usá-lo juntamente com seletores CSS modernos — pense :is(), :has(), :where(), etc. — para ganhar ainda mais controlo das a cascata.

O que é isso sobre seletores CSS modernos?

A Especificação de nível 4 de seletores CSS nos dá algumas maneiras novas e poderosas de selecionar elementos. Alguns dos meus favoritos incluem :is(), :where() e :not(), cada um dos quais é suportado por todos os navegadores modernos e é seguro para uso em quase todos os projetos atualmente.

:is() e :where() são basicamente a mesma coisa, exceto pelo modo como afetam a especificidade. Especificamente, :where() sempre tem uma pontuação de especificidade de 0,0,0. Sim, mesmo :where(button#widget.some-class) não tem especificidade. Enquanto isso, a especificidade :is() é o elemento em sua lista de argumentos com a maior especificidade. Portanto, já temos uma distinção em cascata entre dois seletores modernos com os quais podemos trabalhar.

O incrivelmente poderoso :has() pseudo-classe relacional também é ganhando suporte de navegador rapidamente (e é a maior novidade do CSS desde Grade, na minha humilde opinião). No entanto, no momento da redação deste artigo, o suporte do navegador para :has() ainda não é bom o suficiente para uso em produção.

Deixe-me colocar uma daquelas pseudo-classes no meu BEM e…

/* ❌ specificity score: 0,2,0 */
.something:not(.something--special) {
  /* styles for all somethings, except for the special somethings */
}

Opa! Veja essa pontuação de especificidade? Lembre-se, com o BEM, idealmente, queremos que todos os nossos seletores tenham uma pontuação de especificidade de 0,1,0. Porque é 0,2,0 mau? Considere este mesmo exemplo, expandido:

.something:not(.something--special) {
  color: red;
}
.something--special {
  color: blue;
}

Mesmo que o segundo seletor seja o último na ordem de origem, a especificidade mais alta do primeiro seletor (0,2,0) vence, e a cor de .something--special elementos serão definidos para red. Ou seja, supondo que seu BEM esteja escrito corretamente e o elemento selecionado tenha ambos os .something classe base e .something--special classe modificadora aplicada a ele no HTML.

Usadas sem cuidado, essas pseudoclasses podem impactar o Cascade de maneiras inesperadas. E são esses tipos de inconsistência que podem criar dores de cabeça no futuro, especialmente em bases de código maiores e mais complexas.

Droga. E agora?

Lembre-se do que eu estava dizendo sobre :where() e o fato de que sua especificidade é zero? Podemos usar isso a nosso favor:

/* ✅ specificity score: 0,1,0 */
.something:where(:not(.something--special)) {
  /* etc. */
}

A primeira parte deste seletor (.something) obtém sua pontuação de especificidade usual de 0,1,0. Mas :where() — e tudo dentro dela — tem uma especificidade de 0, o que não aumenta mais a especificidade do seletor.

:where() nos permite aninhar

As pessoas que não se importam tanto quanto eu com a especificidade (e provavelmente são muitas pessoas, para ser justo) se deram muito bem quando se trata de aninhamento. Com alguns toques de teclado despreocupados, podemos acabar com CSS como este (observe que estou usando Sass para abreviar):

.card { ... }

.card--featured {
  /* etc. */  
  .card__title { ... }
  .card__title { ... }
}

.card__title { ... }
.card__img { ... }

Neste exemplo, temos um .card componente. Quando é um cartão “destaque” (usando o .card--featured classe), o título e a imagem do cartão precisam ter um estilo diferente. Mas, como nós agora sabemos, o código acima resulta em uma pontuação de especificidade que é inconsistente com o resto do nosso sistema.

Um nerd de especificidade obstinado poderia ter feito isso:

.card { ... }
.card--featured { ... }
.card__title { ... }
.card__title--featured { ... }
.card__img { ... }
.card__img--featured { ... }

Isso não é tão ruim, certo? Francamente, isso é lindo CSS.

Há uma desvantagem no HTML embora. Os autores experientes do BEM provavelmente estão cientes da lógica desajeitada do modelo necessária para aplicar classes modificadoras condicionalmente a vários elementos. Neste exemplo, o modelo HTML precisa adicionar condicionalmente o --featured classe modificadora para três elementos (.card, .card__title e .card__img) embora provavelmente ainda mais em um exemplo do mundo real. Isso é muito if afirmações.

A :where() selector pode nos ajudar a escrever muito menos lógica de modelo — e menos classes BEM para inicializar — sem aumentar o nível de especificidade.

.card { ... }
.card--featured { ... }

.card__title { ... }
:where(.card--featured) .card__title { ... }

.card__img { ... }
:where(.card--featured) .card__img { ... }

Aqui está a mesma coisa, mas em Sass (observe o e comercial):

.card { ... }
.card--featured { ... }
.card__title { 
  /* etc. */ 
  :where(.card--featured) & { ... }
}
.card__img { 
  /* etc. */ 
  :where(.card--featured) & { ... }
}

Se você deve ou não optar por essa abordagem em vez de aplicar classes modificadoras aos vários elementos filhos, é uma questão de preferência pessoal. Mas pelo menos :where() nos dá a escolha agora!

E o HTML não-BEM?

Não vivemos em um mundo perfeito. Às vezes, você precisa lidar com HTML que está fora de seu controle. Por exemplo, um script de terceiros que injeta HTML que você precisa estilizar. Essa marcação geralmente não é escrita com nomes de classe BEM. Em alguns casos, esses estilos não usam classes, mas IDs!

Mais uma vez, :where() tem as nossas costas. Essa solução é um pouco complicada, pois precisamos referenciar a classe de um elemento em algum lugar mais acima na árvore DOM que sabemos que existe.

/* ❌ specificity score: 1,0,0 */
#widget {
  /* etc. */
}

/* ✅ specificity score: 0,1,0 */
.page-wrapper :where(#widget) {
  /* etc. */
}

No entanto, fazer referência a um elemento pai parece um pouco arriscado e restritivo. E se essa classe pai mudar ou não existir por algum motivo? Uma solução melhor (mas talvez igualmente hacky) seria usar :is() em vez de. Lembre-se, a especificidade de :is() é igual ao seletor mais específico em sua lista de seletores.

Então, em vez de referenciar uma classe que sabemos (ou esperamos!) :where(), como no exemplo acima, poderíamos referenciar uma classe composta e o tag.

/* ✅ specificity score: 0,1,0 */
:is(.dummy-class, body) :where(#widget) {
  /* etc. */
}

O sempre presente body nos ajudará a selecionar nossos #widget elemento e a presença do .dummy-class classe dentro do mesmo :is()body seletor a mesma pontuação de especificidade de uma classe (0,1,0)… e o uso de :where() garante que o seletor não fique mais específico do que isso.

É isso aí!

É assim que podemos aproveitar os recursos modernos de gerenciamento de especificidade do :is() e :where() pseudo-classes juntamente com a prevenção de colisão específica que obtemos ao escrever CSS em um formato BEM. E em um futuro não muito distante, uma vez :has() ganha suporte ao Firefox (atualmente é suportado por um sinalizador no momento da escrita), provavelmente desejaremos emparelhá-lo com :where() para desfazer sua especificidade.

Independentemente de você apostar tudo na nomenclatura BEM ou não, espero que possamos concordar que ter consistência na especificidade do seletor é uma coisa boa!

Carimbo de hora:

Mais de Truques CSS