CSS

28 jan, 2016

Será que realmente precisamos da especificidade no CSS?

1213 visualizações
Publicidade

Ok, antes de eu começar, eu quero deixar uma coisa fora do caminho. Este artigo não é um discurso retórico sobre o quanto eu odeio especificidade. Se você quiser ler um artigo como esse, eu tenho certeza de que você pode encontrar dezenas em outros lugares online.

O que eu quero fazer é colocar uma questão real e honesta, para a comunidade de desenvolvimento web, e espero fazer as pessoas pensarem.

Para reafirmar de forma mais clara: se vivêssemos em um mundo onde a especificidade não foi adicionada à cascade, as coisas seriam melhores ou piores?

Agora eu tenho certeza de que algumas pessoas estão pensando: Quem se importa? Especificidade existe, e estamos presos a ela. Então, qual é o objetivo de trazer isso à tona?

Para quem pensa assim, eu fico feliz em informar que você está errado. 🙂

Neste artigo, vou mostrar que é possível evitar que a especificidade afete a cascade (?) – o que significa que essa questão não é puramente teórica. Se for verdade que a especificidade faz mais mal do que bem, há algo que podemos realmente fazer sobre isso hoje.

Um pouco de história

Para qualquer um que está um pouco enferrujado sobre como funciona a cascade, para regras normais do CSS, a cascade leva três coisas em consideração: ordem de origem, especificidade e importância.

Ordem de origem faz muito sentido. É natural e intuitivo pensar que se a regra Y vem depois da regra X, e ambas as regras se aplicam ao mesmo elemento, as declarações da regra Y devem “ganhar”.

Importância também faz muito sentido. Sempre haverá casos em que você vai precisar sobrepor algo, e é bom ter a opção de fazê-lo. Importância também é a única maneira de sobrepor estilos inline, por isso ela é realmente muito necessária em alguns casos.

Mas eu acho que a especificidade é diferente.

Especificidade não é intuitiva, e – especialmente para os novos desenvolvedores – os resultados muitas vezes podem parecer uma pegadinha, em vez do comportamento desejado. Eu também não tenho certeza de que há um equivalente em outros sistemas ou linguagens.

Por exemplo, e se a especificidade fosse uma coisa no JavaScript? Imagine o quanto mais imprevisível o seu código seria se o seguinte teste passasse.

window.document.foo = 'bar';
document.foo = 'qux';

assert(window.document.foo == 'bar'); // true, WTF?

Seria loucura e completamente incontrolável se a atribuição window.document.foo = ‘bar’ acima (o que vier primeiro) superasse o document.foo = ‘qux’ (que vem em segundo lugar) apenas porque a referência era “mais específica”. No entanto, isso é essencialmente o que acontece em CSS.

Mas especificidade é útil, certo?

Eu tenho certeza de que existem milhares, provavelmente até mesmo milhões, de sites por aí que dependem de especificidade para fazer seus estilos funcionarem. Se os navegadores começarem a ignorar especificidade amanhã, todos esses websites irão quebrar.

Eu não estou sugerindo que a especificidade não está sendo usada; claramente ela está. O que estou sugerindo é que talvez ela não tenha utilidade o suficiente para fazer valer a imprevisibilidade e a confusão que vêm com ela.

Eu acredito que especificidade é útil da mesma forma que as variáveis ​​globais podem ser úteis. E assim como a maioria das pessoas considera a dependência de variáveis ​​globais um anti-pattern, talvez o mesmo aconteça com especificidade.

Além disso, eu não consigo pensar em uma única vez na minha vida em que eu escrevi uma regra CSS na qual eu queria sobrescrever outra regra CSS (usando especificidade), onde eu também não coloquei a regra mais específica mais tarde na ordem no código.

Para deixar essa última afirmação mais clara, eu nunca fiz algo como no exemplo abaixo, onde os links de rodapé mais específicos são definidos antes dos links padrão menos específicos:

.footer a {
  color: white;
  text-decoration: none;
}

a {
  color: blue;
  text-decoration: underline;
}

Se eu quiser que a regra Y sobreponha a regra X, eu a coloco mais tarde na ordem no código. Ponto. O que significa que, em qualquer momento que eu tenha um conflito de especificidade, isso será sempre um acidente.

Assim, isso me fez pensar: se eu espero regras mais tarde na ordem no código para sempre sobrescrever regras anteriores na ordem do código, que tal se eu fizesse isso acontecer independentemente da especificidade dessas regras?

Essencialmente, e se eu pudesse me impedir (e as outras pessoas na minha equipe) de acidentalmente romper com o paradigma de que todos nós concordamos com o que faz mais sentido?

E se a especificidade não existisse?

Vamos imaginar um mundo em que a especificidade não existe em CSS. Nesse mundo, a cascade seria determinada pela ordem no código e pela importância somente. Então, se as regras X e Y correspondem a um elemento particular, e a regra Y vem depois regra X no fonte, a regra Y vai ganhar sempre, independentemente da especificidade da regra X. A única maneira de as propriedades declaradas na regra X sobreporem as propriedades declaradas na regra Y seria a utilização de importância.

Esse seria um mundo melhor? Isso nos levaria a um código mais previsível, sustentável e escalável?

Um exemplo da vida real

Muitas pessoas usam classes “de estado” ou “utilitários” em seu CSS. Um exemplo disso é a classe is-hidden, que é usada para esconder um elemento.

Como as classes de estado podem ser genericamente aplicadas a qualquer elemento, elas costumam ser definidas como um seletor de classe única, o que torna a sua especificidade muito baixa. No entanto, conceitualmente elas são classes do tipo substituição (override-type). Por isso, quero dizer que você não acrescentaria a classe is-hidden a um elemento se você realmente o quisesse visível.

Se não existisse especificidade, você poderia ter certeza de que as classes de estado sobreporiam as outras classes, bastando incluí-las por último no código-fonte. Mas, no momento, você tem que usar !important para resolver o problema.

No final das contas, adicionar !important nessas situações é recomendado quando se estiver usando classes de estado SMACSS e classes de utilitários SUIT. Seria bom se nossas melhores práticas não tivessem que recorrer à opção nuclear para cada necessidade diária de estilo.

Removendo especificidade da cascade

Este é o lugar onde as coisas ficam muito interessantes. Enquanto não é possível simplesmente instruir o navegador a ignorar a especificidade por completo, é possível evitar que a especificidade afete a cascade para um arquivo CSS ou um conjunto de arquivos CSS.

Como? A resposta é fazer com que a especificidade e a ordem de código se tornem a mesma coisa.

Imagine uma folha de estilo em que todas as regras foram definidas da menos específica para a mais específica. Nessa folha de estilo, uma vez que a especificidade das regras também corresponde à ordem de origem ou às regras, a especificidade é levada para fora da equação.

Claro que a maioria das pessoas não escreve seu CSS dessa maneira, e esperar que, ou pedir para que elas o façam não faria sentido.

Mas e se um transpiler pudesse modificar seu CSS, depois de escrito, para garantir que todos os seus seletores estivessem em ordem crescente de especificidade? E, mais importante, se fizesse isso sem afetar em quais elementos esses seletores estão correspondendo?

Graças a algumas peculiaridades impressionantes sobre como seletores CSS trabalham, você pode!

Considere as seguintes regras CSS, atualmente em ordem decrescente de especificidade (acima de cada regra, eu listei a especificidade utilizando a notação [idCount].[classCount].[typeCount]):

/* 0.2.5 */
main.content aside.sidebar ul li a { }

/* 0.1.3 */
aside.sidebar ul a { }

/* 0.1.1 */
.sidebar a { }

Esses seletores pode ser reescrito para estar em ordem crescente de especificidade sem afetar os elementos que eles irão corresponder. Eu destaquei as adições abaixo:

/* 0.2.5 */
main.content aside.sidebar ul li a { }

/* 0.3.3 */
:root:root aside.sidebar ul a { }

/* 0.4.1 */
:root:root:root .sidebar a { }

Isso funciona porque todos os documentos HTML têm um elemento de raiz (o elemento <html>), então adicionar a pseudo-classe :root para o início de um seletor não irá mudar o elemento ao qual ele corresponde.

E uma vez que as pseudo-classes podem ser encadeadas, ou seja :root:root:root ainda irá coincidir com o elemento <html>, você pode acrescentar arbitrariamente especificidade para qualquer seletor para torná-lo mais específico do que o seletor anterior.

Manuseando a ID de especificidade

Seletores de ID são mais específicos do que os seletores de pseudo-classe e, portanto, nenhuma quantidade de :root no início de um seletor irá ganhar de um ID.

No entanto, o seletor de ID #content e o seletor de atributo [id=”content”] irão corresponder exatamente ao mesmo elemento, portanto, se você substituir todos os seletores ID para seletores de atributo, a técnica descrita acima ainda irá funcionar.

Por exemplo, as regras a seguir:

/* 1.1.5 */
main#content aside.sidebar ul li a { }

/* 0.1.3 */
aside.sidebar ul a { }

/* 0.1.1 */
.sidebar a { }

Ficarão assim:

/* 0.2.5 */
main[id="content"] aside.sidebar ul li a { }

/* 0.3.3 */
:root:root aside.sidebar ul a { }

/* 0.4.1 */
:root:root:root .sidebar a { }

Manipulando seletores que já podem estar se referindo ao elemento raiz

Com a maioria dos seletores, não é possível dizer, somente olhando para eles, se eles têm a intenção de corresponder com o elemento <html>. Por exemplo, se um site está usando Modernizr, você provavelmente vai ver os seletores parecidos com isto aqui:

.columns {
  display: flex;
}

.no-flexbox .columns {
  display: table;
}

Para saber a possibilidade de um desses seletores corresponder com o elemento <html>, você teria que incluir ambas as possibilidades e reescrevê-los da seguinte forma:

:root.columns,
:root .columns {
  display: flex;
}

:root.no-flexbox .columns,
:root .no-flexbox .columns {
  display: table;
}

Alternativamente, você pode evitar esse problema ao estabelecer uma convenção de que nenhum de seus seletores tem a permissão de corresponder com o elemento <html> e que <body> é o mais alto que eles podem ir.

O algoritmo reescrito por completo

Eu não conheço qualquer transpiler que atualmente faz o que eu mostrei neste artigo. Se você estiver interessado em tentar escrever um, eu recomendo fazê-lo como o plugin PostCSS. Uma vez que tantas etapas de construção já incluem Autoprefixer (que é um plugin PostCSS), acrescentar isso teria quase nenhum impacto em seu tempo de construção ou complexidade.

Aqui está o algoritmo básico que você precisa seguir:

1 – Iterar através de cada seletor em suas folhas de estilo.

2 – Para cada seletor:

a) Se o seletor contém quaisquer seletores de ID, substitua-os por seletores de atributos ID, senão, não faça nada.

b) Calcule a especificidade do seletor atual (com todos as IDs já substituídas).

c) Se o seletor atual é o primeiro seletor, não faça nada. Senão, compare a sua especificidade com a especificidade do seletor anterior.

d) Se o seletor anterior é menos específico do que o seletor atual, não faça nada. Caso contrário, reescreva o seletor atual de modo que a sua especificidade seja maior ou igual à especificidade do seletor anterior.

Se o seletor atual começa com o seletor :root ou um seletor de tipo diferente de html, reescreva o seletor com :root prefixado como um antepassado. Senão, reescreva o seletor de modo que a primeira parte seja listada tanto vinculada ao :root tanto como um descendente do :root.

3 –  Uma vez que todos os seletores foram reescritos, dê a saída final de todos os estilos. Eles estarão agora em ordem crescente de especificidade.

Potenciais desvantagens

Enquanto a abordagem que eu mostrei acima pode definitivamente funcionar, ela vem com algumas desvantagens. Por um lado, ela irá aumentar o tamanho do arquivo do seu CSS. Isso provavelmente não será um grande problema se você já mantiver a especificidade baixa e escrever suas regras em sua maioria na ordem crescente de especificidade. E, uma vez que é só adicionar a palavra “root” um monte de vezes, isso compacta muito bem, por isso o meu palpite é que a diferença será insignificante.

A outra coisa a se levar em conta são os potenciais problemas com referência a estilos externos. Se você incluir o bootstrap.css de um CDN e, em seguida, usar essa técnica em seus outros estilos, há potencial para algo estranho, já que um conjunto de seletores será reescrito e o outro conjunto, não.

E você não vai necessariamente ser capaz de simplesmente incluir bootstrap.css em sua compilação porque ele pode ou não depender de especificidade para funcionar corretamente.

Pensamentos finais

Antes de concluir, gostaria de reafirmar o que eu disse na introdução. Isso é uma pergunta, não uma prescrição. Eu não tentei essa técnica em produção, já que eu não estou completamente seguro a respeito dela.

Eu suspeito que ela iria simplificar muito as coisas, mas também podemos descobrir o quanto nós realmente dependemos de especificidade.

Se há uma grande equipe lá fora lutando constantemente contra especificidades, seria interessante ouvir se algo como isso ajudaria. Se você tentar fazer isso, por favor, sinta-se à vontade para me informar suas descobertas. Ou, melhor ainda, escreva um artigo de complemento e eu vou linkar a ele.

Atualizações:

  1. Lea Verou me falou, pelo Twitter, que você poderia usar a pseudo-classe :not() como uma alternativa ao uso de :root para aumentar arbitrariamente a especificidade do seletor. A vantagem de usar :not() é que você pode aplicá-lo a qualquer elemento (incluindo <html>), então você não terá que dividir seletores. Você também pode usá-lo para adicionar o tipo ou o nível de especificidade da ID, por exemplo :not(z) ou :not(#z), então você não teria sempre que aumentar as classes.A desvantagem de usar :not() é que você deve ter cuidado ao escolher um seletor que com certeza não coincide com um elemento, caso contrário não vai funcionar.
  2. David Khourshid escreveu uma resposta a este artigo intitulado “A simplicidade da especificidade”, no qual ele argumenta que o CSS não deve ser comparado com uma linguagem imperativa, e as coisas seriam melhores se nós escrevêssemos regras independentes de ordem no código-fonte. Embora provavelmente não é surpresa que eu discordo dessa posição, encorajo a todos a ler, se você quiser uma perspectiva diferente. Esse artigo será publicado em português amanhã aqui no iMasters.

***

Philip Walton faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://philipwalton.com/articles/do-we-actually-need-specificity-in-css/