:has(): o seletor da família

Desde que o tempo começou (em termos de CSS), temos trabalhado com uma cascata em vários sentidos. Nossos estilos compõem uma "Folha de estilo em cascatas". Nossos seletores também funcionam em cascata. Eles podem ficar de lado. Na maioria dos casos, eles vão para baixo. Mas nunca para cima. Durante anos, pensamos em usar um "seletor de pais". E agora, finalmente, ele está chegando! Na forma de um pseudoseletor :has().

A pseudoclasse CSS :has() representa um elemento quando qualquer um dos seletores transmitidos como parâmetros corresponde a pelo menos um elemento.

Mas, é mais do que um "pai" seletor. É uma boa maneira de comercializá-lo. A maneira não tão atraente poderia ser o "ambiente condicional" seletor. Mas isso não é igual. Que tal a "família" seletor?

Compatibilidade com navegadores

Antes de prosseguirmos, vale a pena mencionar o suporte ao navegador. Ainda não chegou lá. Mas estamos quase lá. Ainda não oferecemos suporte ao Firefox, mas estamos trabalhando nisso. Mas ela já está no Safari e deve ser lançada no Chromium 105. Todas as demonstrações neste artigo informam se elas não são compatíveis com o navegador usado.

(link em inglês)

Como usar ":has"

Então, como ela seria? Considere o seguinte HTML com dois elementos irmãos com a classe everybody. Como você selecionaria aquela que tem um descendente com a classe a-good-time?

<div class="everybody">
  <div>
    <div class="a-good-time"></div>
  </div>
</div>

<div class="everybody"></div>

Com :has(), é possível fazer isso com o seguinte CSS.

.everybody:has(.a-good-time) {
  animation: party 21600s forwards;
}

Isso seleciona a primeira instância de .everybody e aplica um animation.

Neste exemplo, o elemento com a classe everybody é o destino. A condição tem um descendente com a classe a-good-time.

<target>:has(<condition>) { <styles> }

No entanto, você pode ir muito além disso, porque o :has() abre muitas oportunidades. Até mesmo aqueles que provavelmente ainda não foram descobertos. Considere algumas delas.

Selecione os elementos figure que tenham um figcaption direto. css figure:has(> figcaption) { ... } Selecione anchors que não tenham um descendente de SVG direto css a:not(:has(> svg)) { ... } Selecione labels que tenham um irmão input direto. De lado! css label:has(+ input) { … } Selecione articles em que um descendente img não tenha texto alt. css article:has(img:not([alt])) { … } Selecione o documentElement em que algum estado está presente no DOM css :root:has(.menu-toggle[aria-pressed=”true”]) { … } Selecionar o contêiner de layout com um número ímpar de filhos css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } Selecionar todos os itens de uma grade que não foram passados css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } Selecione o contêiner que contém um elemento personalizado <todo-list> css main:has(todo-list) { ... } Selecionar todos os solos a em um parágrafo que tenha um elemento irmão direto hr css p:has(+ hr) a:only-child { … } Selecione uma article em que várias condições são atendidas css article:has(>h1):has(>h2) { … } Misture tudo. Selecione uma article em que o título seja seguido por um subtítulo css article:has(> h1 + h2) { … } Selecione :root quando estados interativos forem acionados css :root:has(a:hover) { … } Selecione o parágrafo após uma figure que não tem um figcaption css figure:not(:has(figcaption)) + p { … }

Quais casos de uso interessantes você consideraria para :has()? O fascinante aqui é que isso o encoraja a quebrar seu modelo mental. Isso faz você pensar: "Posso abordar esses estilos de uma maneira diferente?".

Exemplos

Vamos ver alguns exemplos de como podemos usá-la.

Cards

Faça uma demonstração de cartão clássico. Podemos mostrar qualquer informação no card, por exemplo: um título, um subtítulo ou algum tipo de mídia. Este é o cartão básico.

<li class="card">
  <h2 class="card__title">
      <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
</li>
(link em inglês)

O que acontece quando você quer apresentar alguma mídia? Para este design, o cartão pode ser dividido em duas colunas. Antes, era possível criar uma nova classe para representar esse comportamento, por exemplo, card--with-media ou card--two-columns. Esses nomes de classe não apenas se tornam difíceis de evocar, mas também se tornam difíceis de manter e lembrar.

Com o :has(), você pode detectar que o card tem mídia e fazer a ação adequada. Não é necessário usar nomes de classe do modificador.

<li class="card">
  <h2 class="card__title">
    <a href="/article.html">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
</li>

E você não precisa deixá-lo lá. Use a criatividade. Como um card que mostra conteúdo "em destaque" pode se adaptar em um layout? Esse CSS faria com que um cartão em destaque tivesse a largura total do layout e o colocaria no início de uma grade.

.card:has(.card__banner) {
  grid-row: 1;
  grid-column: 1 / -1;
  max-inline-size: 100%;
  grid-template-columns: 1fr 1fr;
  border-left-width: var(--size-4);
}

E se um cartão em destaque com um banner sacudir para chamar atenção?

<li class="card">
  <h2 class="card__title">
    <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
  <div class="card__banner"></div>
</li>
.card:has(.card__banner) {
  --color: var(--green-3-hsl);
  animation: wiggle 6s infinite;
}
(link em inglês)

Muitas possibilidades.

Formulários

E os formulários? Eles são conhecidos por serem complicados de estilizar. Um exemplo disso é a estilização de entradas e seus rótulos. Como sinalizamos que um campo é válido, por exemplo? Com o :has(), isso fica muito mais fácil. Podemos vincular as pseudoclasses de formulário relevantes, por exemplo, :valid e :invalid.

<div class="form-group">
  <label for="email" class="form-label">Email</label>
  <input
    required
    type="email"
    id="email"
    class="form-input"
    title="Enter valid email address"
    placeholder="Enter valid email address"
  />   
</div>
label {
  color: var(--color);
}
input {
  border: 4px solid var(--color);
}

.form-group:has(:invalid) {
  --color: var(--invalid);
}

.form-group:has(:focus) {
  --color: var(--focus);
}

.form-group:has(:valid) {
  --color: var(--valid);
}

.form-group:has(:placeholder-shown) {
  --color: var(--blur);
}

Faça o teste neste exemplo: tente inserir valores válidos e inválidos e focar e desativar o foco.

Também é possível usar :has() para mostrar e ocultar a mensagem de erro de um campo. Adicione uma mensagem de erro ao nosso grupo de campos "e-mail".

<div class="form-group">
  <label for="email" class="form-label">
    Email
  </label>
  <div class="form-group__input">
    <input
      required
      type="email"
      id="email"
      class="form-input"
      title="Enter valid email address"
      placeholder="Enter valid email address"
    />   
    <div class="form-group__error">Enter a valid email address</div>
  </div>
</div>

Por padrão, você oculta a mensagem de erro.

.form-group__error {
  display: none;
}

No entanto, quando o campo se torna :invalid e não está focado, você pode mostrar a mensagem sem a necessidade de nomes de classe extras.

.form-group:has(:invalid:not(:focus)) .form-group__error {
  display: block;
}

Não é possível adicionar um toque extravagante para quando os usuários interagirem com seu formulário. Confira este exemplo. Assista quando inserir um valor válido para a microinteração. Um valor :invalid vai fazer o grupo de formulários tremer. No entanto, apenas se o usuário não tiver preferências de movimento.

Conteúdo

Mencionamos isso nos exemplos de código. Mas como é possível usar :has() no fluxo do documento? Ela lança ideias sobre como podemos estilizar a tipografia nas mídias, por exemplo.

figure:not(:has(figcaption)) {
  float: left;
  margin: var(--size-fluid-2) var(--size-fluid-2) var(--size-fluid-2) 0;
}

figure:has(figcaption) {
  width: 100%;
  margin: var(--size-fluid-4) 0;
}

figure:has(figcaption) img {
  width: 100%;
}

Este exemplo contém figuras. Quando não têm figcaption, eles flutuam dentro do conteúdo. Quando há uma figcaption, elas ocupam a largura total e têm margem extra.

(link em inglês)

Como reagir ao estado

Que tal tornar seus estilos reativos a algum estado em nossa marcação? Considere o exemplo "clássico" barra de navegação deslizante. Se você tiver um botão que abre a navegação, ele pode usar o atributo aria-expanded. O JavaScript pode ser usado para atualizar os atributos apropriados. Quando aria-expanded for true, use :has() para detectar isso e atualizar os estilos da navegação deslizante. O JavaScript faz sua parte, e o CSS pode fazer o que quiser com essas informações. Não é necessário embaralhar a marcação ou adicionar nomes de classe extras etc. (Observação: este não é um exemplo pronto para produção).

:root:has([aria-expanded="true"]) {
    --open: 1;
}
body {
    transform: translateX(calc(var(--open, 0) * -200px));
}
(link em inglês)

O :tem ajuda para evitar erros para o usuário?

O que todos esses exemplos têm em comum? Além do fato de mostrarem maneiras de usar :has(), nenhuma delas exigiu a modificação de nomes de classes. Cada um deles inseriu um novo conteúdo e atualizou um atributo. Esse é um ótimo benefício do :has(), porque ajuda a reduzir os erros do usuário. Com o :has(), o CSS pode assumir a responsabilidade de se ajustar às modificações no DOM. Você não precisa fazer malabarismos com nomes de classes em JavaScript, o que diminui as chances de erros de desenvolvedor. Todos nós já passamos por isso quando digitamos o nome de uma turma digitando um nome e precisamos mantê-lo em pesquisas Object.

É uma ideia interessante e nos leva a uma marcação mais limpa e menos código? Menos JavaScript, já que não estamos fazendo tantos ajustes. Menos HTML, porque você não precisa mais de classes como card card--has-media etc.

Pensar fora da caixa

Como mencionado acima, o :has() incentiva você a quebrar o modelo mental. É uma oportunidade para tentar coisas diferentes. Uma forma de tentar ir além é criando a mecânica do jogo apenas com o CSS. Você pode criar uma mecânica baseada em etapas com formulários e CSS, por exemplo.

<div class="step">
  <label for="step--1">1</label>
  <input id="step--1" type="checkbox" />
</div>
<div class="step">
  <label for="step--2">2</label>
  <input id="step--2" type="checkbox" />
</div>
.step:has(:checked), .step:first-of-type:has(:checked) {
  --hue: 10;
  opacity: 0.2;
}


.step:has(:checked) + .step:not(.step:has(:checked)) {
  --hue: 210;
  opacity: 1;
}

E isso abre possibilidades interessantes. Você pode usá-lo para percorrer um formulário com transformações. É melhor ver esta demonstração em outra guia do navegador.

(link em inglês)

E por diversão, que tal o clássico jogo da buzz wire? A mecânica é mais fácil de criar com :has(). Se o fio ficar parado, o jogo acabou. Sim, podemos criar algumas dessas mecânicas do jogo com recursos como os combinadores irmãos (+ e ~). No entanto, :has() é uma maneira de alcançar esses mesmos resultados sem ter que usar "truques" de marcação interessantes. É melhor ver esta demonstração em outra guia do navegador.

(link em inglês)

Embora você não os coloque em produção tão cedo, eles destacam maneiras de usar o primitivo. Por exemplo, a capacidade de encadear uma :has().

:root:has(#start:checked):has(.game__success:hover, .screen--win:hover)
.screen--win {
  --display-win: 1;
}

Desempenho e limitações

Antes de terminar, o que você não pode fazer com :has()? Há algumas restrições com :has(). Os principais surgem devido aos hits de desempenho.

  • Não é possível :has() um :has(). No entanto, é possível encadear uma :has(). css :has(.a:has(.b)) { … }
  • Nenhum uso de pseudoelemento em :has() css :has(::after) { … } :has(::first-letter) { … }
  • Restringir o uso de :has() dentro de pseudos que aceitam apenas seletores compostos css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • Restringir o uso de :has() após o pseudoelemento css ::part(foo):has(:focus) { … }
  • O uso de :visited sempre vai ser falso css :has(:visited) { … }

Para conferir as métricas de desempenho reais relacionadas ao :has(), confira este Glitch. Agradecemos a Byungwoo por compartilhar esses insights e detalhes sobre a implementação.

Pronto.

Prepare-se para :has(). Conte a seus amigos e compartilhe esta postagem. Ela será um divisor de águas na forma como abordamos o CSS.

Todas as demonstrações estão disponíveis nesta coleção do CodePen.