: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 "Folha de estilo em cascatas". E nossos seletores também. Eles podem ficar nas laterais. Na maioria dos casos, caem. Mas nunca para cima. Por anos, pensamos em um "seletor de pai/mãe". Agora ele finalmente vai chegar. Na forma de um pseudoseletor :has().

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

Mas é mais do que um seletor "pai". Essa é uma boa maneira de comercializá-lo. A maneira não tão atraente pode ser o seletor de "ambiente condicional". Mas aí não tem o mesmo anel. E o seletor "família"?

Compatibilidade com navegadores

Antes de prosseguirmos, vale a pena mencionar o suporte ao navegador. Ele ainda não chegou lá. Mas está se aproximando. Ainda não há compatibilidade com o Firefox, mas planejamos incluir essa funcionalidade. No entanto, ela já está no Safari e será lançada no Chromium 105. Todas as demonstrações deste artigo informarão se não são compatíveis com o navegador usado.

Como usar ":has"

Então, como ele seria? Considere o HTML a seguir com dois elementos irmãos com a classe everybody. Como você selecionaria a 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 da .everybody e aplica uma 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> }

Mas você pode ir muito além disso, porque o :has() abre muitas oportunidades. Mesmo os que provavelmente ainda não foram descobertos. Considere algumas delas.

Selecione elementos figure que tenham um figcaption direto. css figure:has(> figcaption) { ... } Selecione anchors que não têm um descendente direto de SVG. css a:not(:has(> svg)) { ... } Selecione labels que têm um irmão input direto. Indo de lado! css label:has(+ input) { … } Selecione articles em que um img descendente 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”]) { … } 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 passou 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 todos os elementos a em que um único parágrafo/parágrafo selecione cada um em um parágrafo que tenha um irmão direto. articlehrcss p:has(+ hr) a:only-child { … }css article:has(>h1):has(>h2) { … } Selecione uma article com um título seguido por um subtítulo css article:has(> h1 + h2) { … } Selecione :root quando os estados interativos forem acionados css :root:has(a:hover) { … } Selecione o parágrafo que segue uma figure que não tem um figcaption css figure:not(:has(figcaption)) + p { … }

Quais casos de uso interessantes você consegue imaginar para a :has()? O mais fascinante aqui é que incentiva você 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 do cartão clássico. Podemos exibir qualquer informação no card, como título, subtítulo ou alguma 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>
.

O que acontece quando você quer apresentar alguma mídia? Para este design, o cartão poderia ser dividido em duas colunas. Antes, você pode criar uma nova classe para representar esse comportamento, por exemplo, card--with-media ou card--two-columns. Esses nomes de classe não só 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 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>
.

Você não precisa deixar tudo aqui. Mostre sua criatividade. Como um card que exibe conteúdo em destaque pode se adaptar a um layout? Esse CSS alteraria um card em destaque para 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 mexer 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;
}

São tantas possibilidades.

Formulários

E os formulários? Elas são conhecidas por serem difíceis de definir. Um exemplo disso é a estilização de entradas e os rótulos delas. Como sinalizamos que um campo é válido, por exemplo? Com o :has(), isso fica muito mais fácil. Podemos vincular às 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);
}

Faça um teste neste exemplo: insira valores válidos e inválidos e mantenha o foco ativado e desativado.

Também é possível usar :has() para mostrar e ocultar a mensagem de erro em 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 essa mensagem.

.form-group__error {
  display: none;
}

No entanto, quando o campo se tornar :invalid e não estiver focado, você poderá mostrar a mensagem sem a necessidade de outros nomes de classe.

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

Não há razão para você não querer adicionar um toque especial para quando seus usuários interagem com seu formulário. Veja este exemplo. Assista quando inserir um valor válido para a microinteração. Um valor :invalid fará com que o grupo de formulários seja tremido. No entanto, somente se o usuário não tiver preferências de movimento.

.

Conteúdo

Já mencionamos isso nos exemplos de código. Mas como você poderia usar :has() no fluxo do documento? Ele lança ideias sobre como poderíamos aplicar o estilo de tipografia em 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 números. Quando não têm figcaption, eles 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 em nossa marcação. Considere um exemplo com a barra de navegação deslizante "clássica". 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, adicionar outros nomes de classes etc. Observação: esse não é um exemplo pronto para produção.

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

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

O que todos esses exemplos têm em comum? Além do fato de elas mostrarem maneiras de usar :has(), nenhuma delas exigiu modificar os nomes das classes. Cada um deles inseriu um novo conteúdo e atualizou um atributo. Esse é um grande benefício do :has(), porque ajuda a reduzir os erros do usuário. Com :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, criando menos potencial para erro de desenvolvedor. Todos nós já passamos por isso quando digitamos um nome de classe e temos que recorrer a mantê-los em pesquisas Object.

Essa é uma ideia interessante e que nos leva a uma marcação mais limpa e menos código? Menos JavaScript, porque não fazemos tantos ajustes de JavaScript. 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 de tentar coisas diferentes. Uma maneira de tentar ir além é criando mecânicas de jogo só 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;
}

E isso abre possibilidades interessantes. Você pode usar isso para transferir um formulário com transformações. A melhor forma de visualizar essa demonstração é em uma guia separada do navegador.

E, por diversão, que tal o clássico jogo buzz wire? A mecânica é mais fácil de criar com :has(). Se você passar o cursor por cima, o jogo acabou. Sim, podemos criar algumas dessas mecânicas de jogo com coisas como os combinadores irmãos (+ e ~). No entanto, :has() é uma maneira de alcançar esses mesmos resultados sem precisar usar "truques" de marcação interessantes. A melhor forma de visualizar essa demonstração é em uma guia separada do navegador.

.

Embora você não os coloque em produção tão cedo, eles destacam maneiras pelas quais você pode usar o primitivo. Por exemplo, para 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 terminar, o que você não pode fazer com o :has()? Há algumas restrições para :has(). Os principais surgem devido aos hits de desempenho.

  • Não é possível :has() (:has()). Mas você pode 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. Créditos à Byungwoo por compartilhar esses insights e detalhes sobre a implementação.

Pronto!

Prepare-se para :has(). Conte para seus amigos e poste esta postagem, porque será um divisor de águas na nossa abordagem de CSS.

Todas as demonstrações estão disponíveis neste agrupamento CodePen.