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 anchor
s que não têm um descendente direto do SVG
css
a:not(:has(> svg)) { ... }
Selecione label
s que têm um irmão direto input
. Vamos para o lado!
css
label:has(+ input) { … }
Selecione article
s 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 compostoscss ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
- Restringir o uso de
:has()
após o pseudoelementocss ::part(foo):has(:focus) { … }
- O uso de
:visited
sempre será falsocss :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.