: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 os lados. 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 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 CSS a seguir.

.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 SVG direto css a:not(:has(> svg)) { ... } Selecione labels que têm um irmão input direto. Vamos para o lado! css label:has(+ input) { … } Selecione articles em que um descendente img 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 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 tipo 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 alguma 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 as coisas 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, você pode mostrar a mensagem sem precisar 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 têm figcaption, flutuam no conteúdo. Quando um figcaption está presente, ele ocupa toda a largura 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 é preciso fazer malabarismos com nomes de classe no JavaScript, criando menos potencial de erro para o desenvolvedor. Todos nós já cometemos erros de digitação no nome de uma classe e precisamos manter essas pesquisas em 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 do fio elétrico? 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.