:has(): o seletor da família

Desde o início (em termos de CSS), trabalhamos com uma cascata em vários sentidos. Nossos estilos compõem uma "Cascading Style Sheet". E nossos seletores também são em cascata. Eles podem ir para o lado. Na maioria dos casos, elas vão para baixo. Mas nunca para cima. Há anos, sonhamos com um "Seletor de familiares responsáveis". E agora ele finalmente está chegando! Na forma de um pseudoseletor :has().

A pseudoclasse CSS :has() representa um elemento se algum dos seletores transmitidos como parâmetros corresponder a pelo menos um elemento.

Mas ele é mais do que um seletor "pai". Essa é uma boa maneira de divulgar. Uma maneira não tão interessante pode ser o seletor de "ambiente condicional". Mas isso não tem o mesmo som. E o seletor "family"?

Compatibilidade com navegadores

Antes de continuar, vale a pena mencionar o suporte do navegador. Ainda não. Mas está chegando. Ainda não há suporte para o Firefox, mas ele está no roteiro. Mas ele já está no Safari e será lançado no Chromium 105. Todas as demonstrações neste artigo informam se não têm suporte no navegador usado.

Como usar :has

Então, como ela seria? Considere o HTML a seguir com dois elementos irmãos com a classe everybody. Como você selecionaria o 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 é ter um descendente com a classe a-good-time.

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

Mas 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 destas opções.

Selecione elementos figure que tenham um figcaption direto. css figure:has(> figcaption) { ... } Selecione anchors que não têm um descendente direto do SVG css a:not(:has(> svg)) { ... } Selecione labels que têm um irmão direto input. Vamos para o lado! css label:has(+ input) { … } Selecione articles em que um img descendente não tem 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”]) { … } Selecione o contêiner de layout com um número ímpar de filhos css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } Selecione todos os itens em uma grade que não estão com o cursor 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) { ... } Selecione cada a solo em um parágrafo que tenha um elemento hr irmão direto css p:has(+ hr) a:only-child { … } Selecione um article em que várias condições sejam atendidas css article:has(>h1):has(>h2) { … } Misture. Selecione um article em que um título é seguido por um subtítulo css article:has(> h1 + h2) { … } Selecione o :root quando os estados interativos forem acionados css :root:has(a:hover) { … } Selecione o parágrafo que segue um figure que não tem um figcaption css figure:not(:has(figcaption)) + p { … }

Quais casos de uso interessantes você consegue pensar para :has()? O mais interessante é que ele incentiva você a quebrar seu modelo mental. Isso faz você pensar: "Posso abordar esses estilos de maneira diferente?".

Exemplos

Vamos conferir alguns exemplos de como podemos usar essa ferramenta.

Cards

Assista 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 conteúdo de mídia. Confira o card 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>

O que acontece quando você quer introduzir algum tipo de mídia? Nesse design, o card pode ser dividido em duas colunas. Antes, você poderia 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 criar, mas também de manter e lembrar.

Com :has(), você pode detectar que o card tem alguma mídia e fazer a coisa certa. Não é necessário usar nomes de classe de 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 deixar assim. Você pode usar a criatividade. Como um card que mostra conteúdo "em destaque" pode se adaptar a um layout? Esse CSS definiria a largura total do layout para um card de destaque 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 card em destaque com um banner balançar para chamar a 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;
}

Tantas possibilidades.

Formulários

E os formulários? Eles são conhecidos por serem difíceis de estilizar. Um exemplo disso é estilizar entradas e os rótulos delas. Como sinalizamos que um campo é válido? Com :has(), isso fica muito mais fácil. Podemos conectar as pseudoclasses relevantes do formulário, 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);
}

Teste neste exemplo: insira valores válidos e inválidos e ative e desative 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 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;
}

Mas quando o campo se torna :invalid e não está focado, é possível mostrar a mensagem sem a necessidade de nomes de classe extras.

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

Não há motivo para não adicionar um toque de capricho quando os usuários interagem com o formulário. Confira este exemplo. Observe quando você insere um valor válido para a microinteração. Um valor :invalid faz com que o grupo de formulários trema. Mas apenas se o usuário não tiver preferências de movimento.

Conteúdo

Falamos sobre isso nos exemplos de código. Mas como usar :has() no fluxo de documentos? Ele mostra ideias sobre como podemos estilizar a tipografia em torno da mídia, 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 há figcaption, eles flutuam no conteúdo. Quando um figcaption está presente, ele ocupa a largura total e recebe uma margem extra.

Como reagir ao estado

Que tal tornar seus estilos reativos a algum estado na marcação? Considere um exemplo com a barra de navegação deslizante "clássica". Se você tiver um botão que alterna a abertura da navegação, ele poderá usar o atributo aria-expanded. O JavaScript pode ser usado para atualizar os atributos adequados. Quando aria-expanded for true, use :has() para detectar isso e atualizar os estilos da navegação deslizante. O JavaScript faz a parte dele, e o CSS pode fazer o que quiser com essas informações. Não é necessário mudar 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));
}

O :has pode ajudar a evitar erros do usuário?

O que todos esses exemplos têm em comum? Além de mostrar maneiras de usar :has(), nenhuma delas exigiu a modificação de nomes de classe. Cada um deles inseriu novo conteúdo e atualizou um atributo. Esse é um grande benefício do :has(), porque ele pode ajudar a reduzir os erros do usuário. Com :has(), o CSS pode assumir a responsabilidade de se ajustar às modificações no DOM. Não é necessário alternar os nomes de classe em JavaScript, criando menos potencial de erro do desenvolvedor. Todos nós já cometemos erros de digitação no nome de uma classe e precisamos manter esses erros nas pesquisas Object.

É um pensamento interessante, e ele nos leva a uma marcação mais limpa e menos código? Menos JavaScript, porque não estamos fazendo tantos ajustes. Menos HTML, já que 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 maneira de tentar ultrapassar os limites é criar mecânicas de jogo apenas com 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;
}

Isso abre possibilidades interessantes. Você pode usar isso para percorrer um formulário com transformações. É melhor assistir a esta demonstração em uma guia separada do navegador.

E para se divertir, que tal o clássico jogo de arame farpado? A mecânica é mais fácil de criar com :has(). Se o fio for pairado, o jogo acabou. Sim, podemos criar algumas dessas mecânicas de jogo com elementos como os combinatores irmãos (+ e ~). No entanto, o :has() é uma maneira de alcançar os mesmos resultados sem precisar usar "truques" de marcação interessantes. É melhor assistir a esta demonstração em uma guia separada do navegador.

Embora você não vá usar esses recursos na produção tão cedo, eles destacam maneiras de usar a primitiva. Por exemplo, encadear um :has().

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

Desempenho e limitações

Antes de encerrarmos, o que não é possível fazer com :has()? Há algumas restrições com :has(). As principais são causadas por problemas de desempenho.

  • Não é possível :has() um :has(). No entanto, é possível vincular uma :has(). css :has(.a:has(.b)) { … }
  • Nenhum uso de pseudoelemento em :has() css :has(::after) { … } :has(::first-letter) { … }
  • Restrição do 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 será falso css :has(:visited) { … }

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

Pronto.

Prepare-se para :has(). Conte aos seus amigos e compartilhe esta postagem. Ela vai mudar a forma como abordamos o CSS.

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