Aguarde...

27 de maio de 2022

:where() :is() :has()? Novos seletores CSS que facilitam sua vida

:where() :is() :has()? Novos seletores CSS que facilitam sua vida

Quando as pessoas falam sobre a complexidade do CSS, um dos principais contribuintes para isso é a especificidade do CSS, ou escrever seletores CSS eficazes. Quanto mais você adiciona a um seletor CSS, mais preciso ele é, mas também mais específico ele é, então mais difícil será sobrescrever estilos se você precisar em um momento posterior.

Essa faca de dois gumes é o que torna tão difícil escrever bons seletores de CSS: você precisa ser específico, mas não muito específico. É por isso que existem muitas estratégias para escrever bons seletores de CSS ou evitar o problema completamente, de OOCSS a BEM e CSS Atomic.

Este artigo é adaptado de uma palestra que Kilian deu no Web Directions Hover 2022 . Você pode entrar em contato conosco se estiver interessado em que Kilian apresente isso em sua conferência, encontro ou organização.

Neste artigo, porém, não vamos nos concentrar na sintaxe, mas em alguns novos recursos que estão disponíveis para os seletores CSS: as classes :is():where():has()pseudo-classes. Sim, eu os virei no título do artigo porque eles soam mais divertidos dessa maneira.

Um pouco de história

Antes de haver a :is()pseudo-classe, havia a :matches()pseudo-classe. E antes disso havia as * -any pseudo-classes: :-moz-any():-webkit-any().

Você pode se surpreender ao saber que a pseudoclasse * -any existe desde 2010 . Ele foi introduzido no Firefox 4 como , fora de qualquer especificação, e na verdade funcionou praticamente da mesma forma que a classe funciona agora.:-moz-any():is()

O suporte em outros navegadores chegou primeiro como :-webkit-any(), e depois foi adicionado à especificação como :matches(), que também teve suporte em vários navegadores.

Esta :matches()pseudo-classe veio com uma limitação, porém, como o :not()seletor, ela só suportava “seletores simples”.

O que são seletores simples?

Seletores simples são seletores que contêm apenas um único elemento ou propriedade de elemento, como uma classe, atributo, id ou pseudo-classe. Assim que você adiciona um combinador, como um espaço ou ~ ou +, ou um pseudo-elemento (como ::first-letterou ::after), você adiciona uma relação a outro elemento e ele se torna um seletor complexo.

A definição de “seletor simples” mudou entre CSS2 e CSS3: em CSS3 agora há uma diferença entre um “seletor simples”, que é uma coisa ( uma classe, um elemento, etc) e um seletor composto, que é qualquer seletor que não tem combinador.

Isso significa que este seletor CSS é “simples” (embora composto):

p.hover#22[chat~=active]: focus;

e este é “complexo”:

div p

Isso obviamente colocou um limite na utilidade dos seletores :matches()e dos :not()seletores, então, felizmente, ambos foram atualizados em especificações posteriores e agora também suportam seletores complexos, o que significa que podemos usá-los para selecionar elementos com base em suas relações.

E essa não é a única coisa que o :not()seletor contribuiu para essa história. O grupo de trabalho CSS foi renomeado :matches()para :is()porque, por um lado, era mais curto para digitar, mas também forneceu um bom emparelhamento com :not().

É, e não é.

Verificando o suporte para:is()

Isso nos leva ao agora, 2022. A :is()pseudo-classe é suportada em todos os navegadores evergreen, com suporte ausente apenas no IE11 e Opera mini.

Dados sobre suporte para o recurso css-matches-pseudo nos principais navegadores de caniuse.com

Se você quiser testar o suporte, pode usar a @supportsregra com a selector()função, que tem suporte há um pouco mais de tempo :is()e funciona para todas as três pseudo-classes que estamos discutindo neste artigo:

@supports selector(:is(*)) {
  /* your CSS */
}

:is()pseudo-classe

Para direcionar vários caminhos em CSS, você pode adicionar uma vírgula entre vários seletores. Cada seletor será então usado para selecionar elementos.

nav > ul a:hover,
footer > ol a:hover,
aside > p a:hover {
  color: purple;
  text-decoration: underline wavy deeppink;
}

Isso é útil, mas para seletores intimamente relacionados geralmente leva à duplicação. especificamente, a a:hoverparte é a mesma em cada um dos seletores.

Isso é irritante, pois se isso mudar, você terá que corrigir isso em três lugares ao mesmo tempo, mas também há algo mais complicado acontecendo com vários seletores:

Se um deles for inválido, todo o bloco de CSS será descartado.

Agora você pode pensar que seria muito difícil escrever um seletor inválido. Talvez um que nunca se aplique, como :not(*), mas um inválido real?

Bem, considere novos pseudo-estados CSS que são suportados apenas em alguns navegadores, como :fullscreen(o Safari suporta apenas :-webkit-full-screen):

:fullscreen,
:-webkit-full-screen {
  background: red;
}

Para estilizar um elemento de tela cheia no Chrome e Firefox você precisa :fullscreen, mas no Safari você precisa :-webkit-full-screen. Se você colocá-los juntos, o Firefox verá o :-webkit-full-screenseletor, o considerará inválido e descartará tudo, e da mesma forma o Safari o verá :fullscreene o descartará também.

Para estilizar isso em ambos os navegadores de uma maneira que não quebre, você teria que criar dois conjuntos de regras separados e duplicar o estilo em ambos os blocos. Mais duplicação!

:fullscreen {
  background: red;
}

:-webkit-full-screen {
  background: red;
}

Isso significa que as listas de seletores têm dois problemas:

  • Duplicação nos seletores que você escreve, tornando a atualização deles frustrante
  • Se um dos seletores for inválido, tudo será ignorado

Entrar:is()

Resolvendo esses dois problemas com uma única pseudo-classe de duas letras.

Em primeiro lugar, a duplicação, em vez de criar um seletor separado para cada variante aqui, podemos reescrevê-lo para criar mini seletores com a :is()pseudo-classe, e depois temos que escrever o restante do seletor apenas uma vez:

:is(nav > ul, footer > ol, aside > p) a:hover {
  color: purple;
  text-decoration: underline wavy deeppink;
}

Você vê como criamos essencialmente uma lista menor de seletores dentro do :is()? Ao inserir as partes dos seletores que são diferentes, podemos tornar o seletor inteiro muito menor e mais legível.

E a :is()pseudo-classe resolve o segundo problema criando um novo tipo de lista de seletores: o <forgiving-selector-list>. Isso analisará cada seletor individualmente e descartará os que não entender.

Isso significa que podemos agrupar nossas pseudo classes de controle deslizante em um :is()e os navegadores apenas escolherão aquelas que entenderem e ignorarão o resto:

:is(:fullscreen, :-webkit-full-screen) {
  ...
}

Isso é ótimo! O :is()seletor nos poupa de digitar a mesma coisa em vários seletores e também torna nosso CSS mais resiliente ao introduzir uma lista de seletores tolerante.

Filtrando elementos por seus pais

Um recurso legal que aprendi com Stephanie Eckles em sua apresentação Modern CSS Toolkit (você pode encontrar as dicas mencionadas lá no SmolCSS também). Como a :is()pseudoclasse pode receber seletores complexos, você também pode adicionar uma lista de ancestrais e selecionar seu elemento com uma extensão *. Isso significa que esses dois seletores visam o mesmo elemento:

a:is(nav *) {
  ...
}
nav a {
  ...
}

Isso funciona porque o navegador procurará todos os elementos que correspondam a ambos a e que correspondam nav *. O resultado final é que você está realmente selecionando nav a, o elemento a correspondendo a a*.

Aninhando pseudoclasses

Outra consequência do suporte a seletores complexos é que você pode aninhar pseudo classes:

:is(div:not(.demo) nav) {
  ...
}

O que este seletor seleciona é:

  1. Todos os navelementos,
  2. Que estão em um div,
  3. Mas só se isso divnão tiver uma classe de “demo”.

Sobre :not()

:not() quebrará se contiver seletores inválidos porque a especificação afirma que deve usar uma lista de seletores “regular”, não uma lista de seletores “perdoável” como :is().

A solução para isso é aninhar um :is()seletor em seu :not()seletor, embora seja melhor se a especificação for atualizada para que o seletor not também use uma lista de seletores perdoadora.

/* breaks */
not(:fullscreen, :-webkit-full-screen) {
  ...
}

/* works */
not(:is(:fullscreen, :-webkit-full-screen)) {
  ...
}

Coisas para observar com:is()

:is()própria pseudo-classe também vem com algumas pegadinhas:

Sem suporte para pseudo-elementos

Você não pode selecionar pseudo-elementos como ::before::after, pois eles não são elementos presentes no DOM e, portanto, não podem “corresponder”.

/* won't work */
a:is(::before, ::after) {
  ...
}

/* You have to write */
a::before,
a::after {
  ...
}

Os espaços são significativos

O espaço faz parte do seletor, então você deve ter isso em mente.

h1:is(:hover, :focus) {
  ...
}
/* h1:hover, h1:focus { ... } */

h1 :is(:hover, :focus) {
  ...
}
/* h1 *:hover, h1 *:focus { ... } */

No exemplo acima, a linha superior h1:is(:hover, :focus)avaliará o foco e o foco aplicado ao, h1mas adicionando um espaço entre o h1 e o seletor is avalia o foco e o foco aplicado a qualquer descendente. O seletor universal ( *) é adicionado implicitamente.

Especificidade

O tópico CSS favorito de todos, e :is()faz algo interessante com ele.

A especificidade em seletores é expressa em três números: o primeiro é para IDs, o segundo para classes e pseudo-classes e o terceiro para elementos e pseudo-elementos. Qualquer número prevalecerá sobre os posteriores, então você pode ter uma centena de classes e ainda assim um único ID venceria em termos de especificidade.

  • #id= (1,0,0)
  • .class= (0,1,0)
  • div= (0,0,1)

Se você quiser explorar isso por si mesmo, criamos uma calculadora de especificidade on -line que explica visualmente de quais partes seu seletor é construído e explica a pontuação.

Para determinar a especificidade de um :is()seletor, os navegadores não apenas pegam o valor padrão de uma pseudoclasse (0,1,0), mas eles pegam o valor do seletor mais específico na pseudoclasse is. Isso significa que você pode facilmente eliminar sua especificidade sem prestar atenção a ela.

/* regular pseudo-class */
:first-child {
} /* (0,1,0) */

/* :is pseudo-class */
:is(h1, ul.nav, #message) {
} /* (1,0,0) */

A especificidade desse segundo exemplo acaba sendo (1,0,0) por causa desse seletor de ID, mesmo que esse seletor não corresponda a nenhum elemento em seu DOM.

O seletor mais específico determina a especificidade.

E se não houver como usar um seletor de alta especificidade? Isso nos leva à próxima pseudoclasse da nossa lista.

:where()pseudo-classe

:where()foi introduzido um pouco mais tarde, mas agora é tão bem suportado quanto o is()seletor: todos os navegadores evergreen, mas sem suporte no IE11 ou Opera mini.

Ele foi criado para resolver essa questão de especificidade. Funciona exatamente da mesma forma que a :is()pseudoclasse com uma diferença fundamental: sua especificidade sempre será resolvida para (0,0,0), independentemente dos seletores dentro dela.

:where(div) /* (0,0,0) */
:where(.class) /* (0,0,0) */
:where(#id) /* (0,0,0) */

Isso pode ajudá-lo a escapar de problemas em que seu único recurso seria um seletor ainda mais específico ou adicionar um !importanta uma regra CSS.

Embora útil para questões de especificidade, há outra área em que acho que a :where()pseudoclasse terá um grande impacto: em bibliotecas e redefinições de CSS.

Uma alternativa mais simples para camadas em cascata

Com a :where()pseudo classe, as bibliotecas podem enviar seus estilos com especificidade zero. Você obtém todos os benefícios de uma biblioteca sem ter que lidar com a especificidade para sobrescrever seu estilo. Open Props é uma biblioteca CSS que já aplica esse padrão.

Embora as camadas em cascata possam estar recebendo mais atenção ultimamente como uma solução potencial para o mesmo problema de especificidade e estruturas CSS, acho que a eficácia :where()não deve ser subestimada. `:where() pode ser aplicado em um nível de seletor e não requer que você altere e tenha em mente toda a sua arquitetura CSS da maneira que as camadas em cascata fazem.

Então é isso :is():where(). Essas duas pseudo classes nos ajudam a reduzir a duplicação de seletores, perdoam quando se trata de seletores inválidos, algo que pode ser particularmente útil no suporte entre navegadores e, no caso de :where(), nos ajuda a gerenciar a especificidade de um seletor.

Ambos :is():where()são suportados em todos os navegadores evergreen e têm sido usados ​​em algumas versões, portanto, eles são seguros de usar se você não precisar mais oferecer suporte ao Internet Explorer, o que espero que não.

Isso nos leva à última pseudo-classe deste artigo. É o mais novo e sem suporte, mas é aquele que os desenvolvedores pedem desde, bem, desde sempre:

:has()pseudo-classe: o seletor pai.

Deixe-me explicar um pouco o “seletor pai”. Com seletores CSS você  estiliza elementos por seus pais, mas o que as pessoas querem dizer quando dizem que um seletor pai é que elas querem estilizar um elemento com base em quais outros elementos estão dentro dele. Por exemplo, eles querem dizer algo assim:

  /* ↓ Select this */
this(div) img
        /* ↑ not this */

para especificar que desejam selecionar divs, mas somente se contiverem imagens.

Embora tenha havido muitas propostas ao longo dos anos, isso foi considerado impossível ou muito intensivo em desempenho para implementar devido à maneira como os seletores CSS são analisados: eles começam no final.

/* Starts here ↓ */
header h1#sitetitle > .logo a {
}
/* ↑ Not here  */

Eles são analisados ​​dessa maneira porque os navegadores começam com o elemento e tentam encontrar todo o CSS que se aplica a ele.

É mais rápido invalidar regras que não se aplicam a um determinado elemento quando você começa no final de um seletor, porque você não precisa percorrer todos os descendentes em potencial apenas para descobrir que o elemento no final não corresponde .

Começando pelo final, você pode começar descartando todos os elementos que não são aelementos (neste caso). Isso reduz o número total de elementos que você precisará considerar. A partir daí, você pode subir na árvore um ancestral de cada vez para descartar quaisquer outros elementos não correspondentes.

Enquanto alguns veteranos podem agora ter flashbacks de argumentos antigos sobre desempenho, plugins jQuery e afins, é 2022 e a :has()pseudo-classe nos permite selecionar elementos dependendo de seus descendentes, nativamente no navegador, sem implicações perceptíveis no desempenho.

Então, primeiro as más notícias:

Suporte ao navegador

:has()seletor está disponível no Safari, e em outros navegadores Chromium, mas apenas com os recursos experimentais da plataforma da Web ativados. Portanto, ainda não está disponível no Chrome ou Firefox.

Dados sobre suporte para o recurso css-has nos principais navegadores de caniuse.com

O Firefox, pelo que posso dizer, ainda não começou a trabalhar nele. Portanto, não espere poder usar isso na natureza em um futuro próximo.

Quando eu disse que não há implicações de desempenho, o que eu quis dizer foi que não havia implicações de desempenho no Webkit e no Blink. Cada mecanismo de renderização tem otimizações diferentes, então a implementação no mecanismo gecko do Firefox pode acabar sendo mais difícil.

Claro, isso não vai nos impedir de explorar suas muitas possibilidades, porque como Bramus disse, a pseudoclasse CSS Has é muito mais do que um seletor pai . Na especificação, ela é chamada de pseudoclasse relacional, e veremos o porquê daqui a pouco.

A sintaxe

Primeiro, porém, a sintaxe. Para voltar ao exemplo que acabei de dar, de selecionar todos os divs que contêm uma imagem, veja como fica com :has(). Selecionamos todas as divs, mas adicionamos uma condicional de que elas precisam ter imagens dentro delas.

/* ↓ Select this */
div: has(img);
/* ↑ not this */

Como :is():where(), você também pode usar seletores complexos. Como divs que têm imagens como irmãos.

div:has(~ img) {
  ...
}

ou vários seletores, como divs que possuem imagens, vídeos ou svgs como descendentes.

div:has(img, video, svg) {
  ...
}

Também gosto :is():where()a lista de seletores é perdoadora. Portanto div:has(img, video, svg:undefined), com um pseudo “:undefined” que não existe, ainda selecionará todos os divs que possuem imagens ou elementos de vídeo neles.

Especificidade

A especificidade de uma :haspseudoclasse é determinada da mesma forma que para as pseudoclasses e: o elemento mais específico determina a especificidade de toda a pseudoclasse :is().:not()

div: has(img, .logo, #brand); /* (1,0,0) */

Usando :has()como parte de um seletor

Ao usar :has()em diferentes partes do seu seletor, você pode não apenas selecionar elementos com base em seus descendentes, mas também selecionar elementos com base em seus irmãos ou mesmo primos.

Ou em outras palavras, podemos selecionar elementos com base em sua relação com uma parte diferente do DOM, a pseudoclasse relacional.

Por exemplo, digamos que você tenha um elemento de cartão e queira estilizar o h3 de maneira diferente se a imagem do cartão estiver presente:

<div class="card">
  <div class="imagecontainer">
    <img src="" alt="" />
  </div>
  <div class="textcontainer">
    <h3>Title</h3>
    <p>...</p>
  </div>
</div>

Podemos fazer isso fazendo :has()parte do seletor que segmenta esse h3. Para direcionar o h3que normalmente escrevemos .card h3. Em seguida, adicionamos uma condicional :has()ao cartão:

.card:has(.imagecontainer img) h3 {
  ...
}

Este seletor corresponde a:

  1. Todos os elementos h3,
  2. Que estão em elementos com o cartão de classe,
  3. Mas somente se esse elemento do cartão também tiver:
    1. Uma imagem
    2. Em um elemento com a classe imagecontainer.

Também podemos inverter que: estilize de forma h3diferente se não houver nenhuma imagem presente, aninhando uma pseudo-classe `:not().

.card:has(:not(.imagecontainer img)) h3 {
  ...
}

Isso oferece muito mais opções de estilo para situações em que você teria adicionado anteriormente algo como class="has_image"ao seu cartão.

Reimplementando:focus-within

Você também pode usar :has()para implementar :focus-within, que permite segmentar o elemento se algum de seus descendentes tiver foco. Ambos agem da mesma forma:

form:focus-within {
  ...
}

form:has(*:focus) {
  ...
}

Embora com :has()nós possamos ir um passo além, como apenas estilizar o formulário se for um elemento específico que tem foco. Isso não é possível com foco interno:

form:has(input[type='radio']:focus) {
  ...
}

Mostrando um campo de entrada “outro” sem JS

Também funciona para outros pseudos, como :checked. Com isso podemos implementar um campo de entrada “outro” que aparece quando uma opção específica é marcada sem escrever uma única linha de JS:

form:has(input.my-checkbox:checker) input.my-textfield {
  display: block;
}

Estilizando formulários se eles tiverem erros

Você também pode puxar coisas como :valid:invalid:user-invalidpseudo-classes para estilizar um formulário de forma diferente:

form:has(input:user-invalid) {
  border: 3px dashed red;
}

Ou até mesmo para mostrar uma lista de campos de formulário inválidos ou ausentes na parte superior ou inferior do formulário:

form:has(.name:user-invalid) li.name-warning {
  display: block;
}

Há muitos outros exemplos nos quais você pode pensar, mas espero que isso tenha lhe dado uma boa ideia do potencial que a :has()pseudo-classe traz.

O futuro dos seletores CSS

Estou muito ansioso para todas as coisas inteligentes que as pessoas vão inventar usando :is():where():has(), e não acho que os exemplos acima nem arranham a superfície.

Especialmente :has()vai ter uma imagem muito grande nos seletores CSS que você escreverá nos próximos anos.

Postado em Blog
Escreva um comentário