Aguarde...

27 de agosto de 2022

Usando :has() como um seletor de pais CSS e muito mais

Usando :has() como um seletor de pais CSS e muito mais

Tem sido um sonho de longa data dos desenvolvedores front-end ter uma maneira de aplicar CSS a um elemento com base no que está acontecendo dentro desse elemento.

Talvez queiramos aplicar um layout a um elemento de artigo se houver uma imagem principal no topo e um layout diferente se não houver uma imagem principal. Ou talvez queiramos aplicar estilos diferentes a um formulário dependendo do estado de um de seus campos de entrada. Que tal dar a uma barra lateral uma cor de fundo se houver um determinado componente nessa barra lateral e uma cor de fundo diferente se esse componente não estiver presente? Casos de uso como esses já existem há muito tempo, e os desenvolvedores da Web abordaram repetidamente o CSS Working Group, implorando para que inventassem um “seletor pai”.

Nos últimos vinte anos , o Grupo de Trabalho CSS discutiu a possibilidade muitas e muitas vezes. A necessidade era clara e bem compreendida. Definir a sintaxe era uma tarefa factível. Mas descobrir como um mecanismo de navegador poderia lidar com padrões circulares potencialmente muito complexos e realizar os cálculos com rapidez suficiente parecia impossível. As primeiras versões de um seletor pai foram elaboradas para CSS3, apenas para serem adiadas. Finalmente, a :has() pseudoclasse foi definida oficialmente em CSS Selectors nível 4 . Mas ter um padrão web por si só não se tornou :has()realidade. Ainda precisávamos de uma equipe de navegadores para descobrir o verdadeiro desafio de desempenho. Enquanto isso, os computadores continuaram a ficar mais poderosos e mais rápidos ano após ano.

Em 2021, Igalia começou a defender :has()entre as equipes de engenharia de navegadores, prototipando suas ideias e documentando suas descobertas em relação ao desempenho. A atenção renovada :has()chamou a atenção dos engenheiros que trabalham no WebKit da Apple. Começamos a implementar a pseudo-classe, pensando nas possibilidades de melhorias de desempenho necessárias para fazer isso funcionar. Debatemos se começaríamos com uma versão mais rápida com um escopo muito limitado e estreito do que ela poderia fazer, e então tentar remover esses limites se possível… ou começar com algo que não tivesse limites, e apenas aplicaria restrições conforme necessário. Nós fomos em frente e implementamos a versão mais poderosa. Desenvolvemos uma série de novidades:has-otimizações específicas de cache e filtragem, e aproveitou as estratégias avançadas de otimização existentes do nosso mecanismo CSS. E nossa abordagem funcionou, provando que após uma espera de duas décadas, finalmente é possível implementar tal seletor com desempenho fantástico , mesmo na presença de grandes árvores DOM e grande número de :has()seletores.

A equipe do WebKit foi lançada :has()no Safari Technology Preview 137 em dezembro de 2021 e no Safari 15.4 em 14 de março de 2022. A Igalia fez o trabalho de engenharia para implementar :has()no Chromium, que será lançado no Chrome 105 em 30 de agosto de 2022. Presumivelmente, os outros navegadores criados no Chromium não ficará muito atrás. A Mozilla está atualmente trabalhando na implementação do Firefox.

Então, vamos dar uma olhada prática passo a passo no que os desenvolvedores da web podem fazer com essa ferramenta desesperadamente desejada . Acontece que a :has()pseudo-classe não é apenas um “seletor pai”. Após décadas de becos sem saída, este seletor pode fazer muito mais.

O básico de como usar :has() como seletor pai

Vamos começar com o básico. Imagine que queremos estilizar um <figure>elemento com base no tipo de conteúdo na figura. Às vezes, nossa figura envolve apenas uma imagem.

<figure>
  <img src="flowers.jpg" alt="spring flowers">
</figure>

Enquanto outras vezes há uma imagem com uma legenda.

<figure>
  <img src="dog.jpg" alt="black dog smiling in the sun">
  <figcaption>Maggie loves being outside off-leash.</figcaption>
</figure>

Agora vamos aplicar alguns estilos ao figureque só serão aplicados se houver um figcaptiondentro da figura.

figure:has(figcaption) {
  background: white;
  padding: 0.6rem;
}

Este seletor significa o que diz – qualquer figureelemento que tenha um figcaptioninterior será selecionado.

Aqui está a demonstração, se você quiser alterar o código e ver o que acontece. Certifique-se de usar um navegador compatível:has() – a partir de hoje, esse é o Safari.

Nesta demonstração, também direciono qualquer um figureque contenha um preelemento usando figure:has(pre).

figure:has(pre) { 
  background: rgb(252, 232, 255);
  border: 3px solid white;
  padding: 1rem;
}

E eu uso uma consulta de recurso de seletor para ocultar um lembrete sobre o suporte do navegador sempre que o navegador atual oferecer suporte a arquivos :has().

@supports selector(:has(img)) {
  small {
    display: none;
  }
}

@supports selector()regra em si é muito bem suportada . Pode ser incrivelmente útil sempre que você quiser usar uma consulta de recurso para testar o suporte do navegador de um seletor específico.

E, finalmente, nesta primeira demonstração, também escrevo um seletor complexo usando o :not() pseudo-class. Eu quero aplicar display: flexà figura – mas apenas se uma imagem for o único conteúdo. O Flexbox faz com que a imagem estique para preencher todo o espaço disponível.

Eu uso um seletor para direcionar qualquer um figureque não tenha nenhum elemento que não seja uma imagem. Se figuretiver um figcaptionprepou um h1— ou qualquer elemento além disso img— então o seletor não se aplica.

figure:not(:has(:not(img))) {
  display: flex;
}

:has()é uma coisa poderosa.

Um exemplo prático usando :has() com CSS Grid

Vejamos uma segunda demonstração em que usei :has()como seletor pai para resolver facilmente uma necessidade muito prática.

Eu tenho vários cartões de teaser de artigo definidos usando CSS Grid. Alguns cartões contêm apenas títulos e texto, enquanto outros também têm uma imagem. Quero que os cartões com imagens ocupem mais espaço na grade do que aqueles sem imagens.

Não quero ter que fazer trabalho extra para fazer meu sistema de gerenciamento de conteúdo aplicar uma classe ou usar JavaScript para layout. Eu só quero escrever um seletor simples em CSS que dirá ao navegador para fazer qualquer cartão teaser com uma imagem para ocupar duas linhas e duas colunas na grade.

:has()pseudo-classe torna isso simples:

article:has(img) {
  grid-column: span 2;
  grid-row: span 2;
}

Essas duas primeiras demos usam seletores de elementos simples dos primeiros dias do CSS, mas todos os seletores podem ser combinados com :has(), incluindo o seletor de classe , o seletor de ID , o seletor de atributo — e combinadores poderosos.

Usando :has() com o combinador filho

Primeiro, uma rápida revisão da diferença entre o combinador descendente e o combinador filho ( >).

O combinador descendente existe desde o início do CSS. É o nome chique para quando colocamos um espaço entre dois seletores simples. Assim:

a img { ... }

Isso tem como alvo todos os imgelementos contidos em um aelemento, não importa a distância entre the ae the imgna árvore HTML DOM.

<a>
  <figure>
    <img src="photo.jpg" alt="don't forget alt text" width="200" height="100">
  </figure>
</a>

Combinador filho é o nome para quando colocamos um >entre dois seletores — que diz ao navegador para direcionar qualquer coisa que corresponda ao segundo seletor, mas apenas quando o segundo seletor é um filho direto do primeiro.

a > img { ... }

Por exemplo, este seletor tem como alvo todos os imgelementos envolvidos por um aelemento, mas somente quando o imgestá imediatamente após o ano HTML.

<a>
  <img src="photo.jpg" alt="don't forget alt text" width="200" height="100">
</a>

Com isso em mente, vamos considerar a diferença entre os dois exemplos a seguir. Ambos selecionam o aelemento, em vez do img, já que estamos usando :has().

a:has(img) { ... }
a:has(> img) { ... }

O primeiro seleciona qualquer aelemento com um imginterior — qualquer lugar na estrutura HTML. Enquanto o segundo seleciona um elemento somente se imgfor um filho direto do a.

Ambos podem ser úteis; eles realizam coisas diferentes.

Existem dois tipos adicionais de combinadores – ambos são irmãos. E é através deles que :has()se torna mais do que um seletor pai.

Usando :has() com combinadores irmãos

Vamos revisar os dois seletores com relacionamentos entre irmãos. Há o combinador do próximo irmão ( +) e o combinador do irmão subsequente ( ~).

O combinador próximo ( +) seleciona apenas os parágrafos que vêm diretamente após um h2elemento.

h2 + p
<h2>Headline</h2>
<p>Paragraph that is selected by `h2 + p`, because it's directly after `h2`.</p>

O combinador de irmão subsequente ( ~) seleciona todos os parágrafos que vêm após um h2elemento. Eles devem ser irmãos, mas pode haver qualquer número de outros elementos HTML entre eles.

h2 ~ p
<h2>Headline</h2>
<h3>Something else</h3>
<p>Paragraph that is selected by `h2 ~ p`.</p>
<p>This paragraph is also selected.</p>

Observe que h2 + ph2 ~ pselecione os elementos do parágrafo, e não os h2títulos. Como outros seletores (pense em a img), é o último elemento listado que é direcionado pelo seletor. Mas e se quisermos segmentar o h2? Podemos usar combinadores irmãos com:has().

Com que frequência você quis ajustar as margens de um título com base no elemento que o segue? Agora é fácil. Este código nos permite selecionar qualquer h2um com um pimediatamente após ele.

h2:has(+ p) { margin-bottom: 0; }

Incrível.

E se quisermos fazer isso para todos os seis elementos do título, sem escrever seis cópias do seletor. Podemos usar :is para simplificar nosso código.

:is(h1, h2, h3, h4, h5, h6):has(+ p) { margin-bottom: 0; }

Ou se quisermos escrever este código para mais elementos do que apenas parágrafos? Vamos eliminar a margem inferior de todos os títulos sempre que forem seguidos por parágrafos, legendas, exemplos de código e listas.

:is(h1, h2, h3, h4, h5, h6):has(+ :is(p, figcaption, pre, dl, ul, ol)) { margin-bottom: 0; }

A combinação :has()com combinadores descendentes , combinadores filhos ( >), combinadores irmãos próximos ( +) e combinadores irmãos subsequentes ( ~) abre um mundo de possibilidades. Mas oh, isso ainda é apenas o começo.

Estilizando estados de formulário sem JS

Existem muitas pseudo-classes fantásticas que podem ser usadas dentro de has:(). Na verdade, revoluciona o que as pseudo-classes podem fazer. Anteriormente, as pseudoclasses eram usadas apenas para estilizar um elemento com base em um estado especial — ou estilizar um de seus filhos. Agora, pseudo-classes podem ser usadas para capturar o estado, sem JavaScript, e estilizar qualquer coisa no DOM com base nesse estado.

Os campos de entrada de formulário fornecem uma maneira poderosa de capturar esse estado. As pseudoclasses específicas de formulário incluem :autofill:enabled:disabled:read-only:read-write:placeholder-shown:default:checked:indeterminate:valid:invalid:in-range:out-of-range:required.:optional

Vamos resolver um dos casos de uso que descrevi na introdução — a longa necessidade de estilizar um rótulo de formulário com base no estado do campo de entrada. Vamos começar com um formulário básico.

<form>
  <div>
    <label for="name">Name</label> 
    <input type="text" id="name">
  </div>
  <div>
    <label for="site">Website</label> 
    <input type="url" id="site">
  </div>
  <div>
    <label for="email">Email</label>
    <input type="email" id="email">
  </div>
</form>

Gostaria de aplicar um plano de fundo a todo o formulário sempre que um dos campos estiver em foco.

form:has(:focus-visible) { 
  background: antiquewhite;
}

Agora eu poderia ter usado form:focus-withinem vez disso, mas se comportaria como form:has(:focus). A :focus pseudo-classe sempre aplica CSS sempre que um campo está em foco. A :focus-visible pseudo-classe fornece uma maneira confiável de estilizar um indicador de foco apenas quando o navegador desenha um nativamente, usando a mesma heurística complexa que o navegador usa para determinar se deve ou não aplicar um anel de foco.

Agora, vamos imaginar que eu queira estilizar os outros campos, aqueles que não estão em foco — alterando a cor do texto do rótulo e a cor da borda de entrada. Antes :has(), isso exigia JavaScript. Agora podemos usar este CSS.

form:has(:focus-visible) div:has(input:not(:focus-visible)) label {
  color: peru;
}
form:has(:focus-visible) div:has(input:not(:focus-visible)) input {
  border: 2px solid peru;
}

O que esse seletor diz? Se um dos controles dentro deste formulário tiver foco e o elemento de entrada para esse controle de formulário específico não tiver foco, altere a cor do texto desse rótulo para peru. E altere a borda do campo de entrada para 2px solid peru.

Você pode ver esse código em ação na demonstração a seguir clicando dentro de um dos campos de texto. O plano de fundo do formulário muda, como descrevi anteriormente. E as cores do rótulo e da borda de entrada dos campos que não estão em foco também mudam.

Nesta mesma demonstração, também gostaria de melhorar o aviso ao usuário quando há um erro no preenchimento do formulário. Durante anos, conseguimos colocar facilmente uma caixa vermelha em torno de uma entrada inválida com este CSS.

input:invalid {
  outline: 4px solid red;
  border: 2px solid red;
} 

Agora com :has(), podemos tornar o texto do rótulo vermelho também:

div:has(input:invalid) label {
  color: red;
}

Você pode ver o resultado digitando algo no site ou campo de e-mail que não seja um URL ou endereço de e-mail totalmente formado. Ambos são inválidos e, portanto, ambos acionarão uma borda vermelha e um rótulo vermelho, com um “X”.

Alternar modo escuro sem JS

E por último, nesta mesma demonstração estou usando uma caixa de seleção para permitir que o usuário alterne entre um tema claro e escuro.

body:has(input[type="checkbox"]:checked) {
  background: blue;
  --primary-color: white;
}
body:has(input[type="checkbox"]:checked) form { 
  border: 4px solid white;
}
body:has(input[type="checkbox"]:checked) form:has(:focus-visible) {
  background: navy;
}
body:has(input[type="checkbox"]:checked) input:focus-visible {
  outline: 4px solid lightsalmon;
}

Estilizei a caixa de seleção do modo escuro usando estilos personalizados, mas ainda parece uma caixa de seleção. Com estilos mais complexos, eu poderia criar uma alternância em CSS .

De maneira semelhante, eu poderia usar um menu de seleção para fornecer ao usuário vários temas para o meu site.

body:has(option[value="pony"]:checked) {
  --font-family: cursive;
  --text-color: #b10267;
  --body-background: #ee458e;
  --main-background: #f4b6d2;
}

Sempre que houver uma oportunidade de usar CSS em vez de JavaScript, eu aproveito. Isso resulta em uma experiência mais rápida e um site mais robusto. JavaScript pode fazer coisas incríveis, e devemos usá-lo quando for a ferramenta certa para o trabalho. Mas se conseguirmos o mesmo resultado apenas em HTML e CSS, melhor ainda.

E mais

Olhando através de outras pseudo-classes , há tantas que podem ser combinadas com :has(). Imagine as possibilidades com :nth-child:nth-last-child:first-child:last-child:only-child:nth-of-type:nth-last-of-type:first-of-type:last-of-type:only-of-type. A nova :modal pseudoclasse é acionada quando a dialogestá no estado aberto. Com :has(:modal)você pode estilizar qualquer coisa no DOM com base no fato de dialogestar aberto ou fechado.

No entanto, nem todas as pseudoclasses são atualmente suportadas :has()em todos os navegadores, portanto, experimente seu código em vários navegadores. Atualmente as pseudoclasses de mídia dinâmica não funcionam — como :playing:paused:muted, etc. Elas podem muito bem funcionar no futuro, então se você estiver lendo isso no futuro, teste-as! Além disso, o suporte à invalidação de formulário está ausente em determinadas situações específicas, portanto, as alterações de estado dinâmicas para essas pseudoclasses podem não ser atualizadas com o :has().

O Safari 16 adicionará suporte para :has(:target)abrir possibilidades interessantes para escrever código que procure na URL atual um fragmento que corresponda ao ID de um elemento específico. Por exemplo, se um usuário clicar em um índice na parte superior de um documento e pular para a seção da página correspondente a esse link, :targetoferece uma maneira de estilizar esse conteúdo de forma exclusiva, com base no fato de o usuário ter clicado no link para chegar lá. E :has()abre o que esse estilo pode fazer.

Algo a ser observado – o CSS Working Group resolveu proibir todos os pseudo-elementos existentes dentro do :has(). Por exemplo, article:has(p::first-line)ol:has(li::marker)não vai funcionar. O mesmo com ::beforee::after.

A revolução :has()

Isso parece uma revolução na forma como vamos escrever seletores CSS, abrindo um mundo de possibilidades anteriormente impossíveis ou muitas vezes não valem o esforço. Parece que, embora possamos reconhecer imediatamente o quão útil :has()será, também não temos ideia do que é realmente possível. Nos próximos anos, as pessoas que fizerem demonstrações e mergulharem profundamente no que o CSS pode fazer terão ideias incríveis, chegando :has()ao limite.

Michelle Barker criou uma demonstração fantástica que aciona a animação dos tamanhos das trilhas de grade por meio do uso de :has()e estados de foco.  O suporte para faixas de grade animadas será lançado no Safari 16 . Você pode experimentar esta demonstração hoje no Safari Technology Preview ou no Safari 16 beta .

A parte mais difícil :has()será abrir nossas mentes para suas possibilidades. Estamos tão acostumados com os limites impostos a nós por não ter um seletor pai. Agora, temos que quebrar esses hábitos.

Essa é mais uma razão para usar CSS vanilla, e não se limitar às classes definidas em um framework. Ao escrever seu próprio CSS, personalizado para seu projeto, você pode aproveitar totalmente todas as poderosas capacidades dos navegadores atuais

Postado em Blog
Escreva um comentário