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 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 SVG direto
css
a:not(:has(> svg)) { ... }
Selecione label
s que têm um irmão input
direto. Vamos para o lado!
css
label:has(+ input) { … }
Selecione article
s 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.
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 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. Pegue nosso grupo de campos "e-mail" e adicione uma mensagem de erro a ele.
<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, é possível 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 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 é 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 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.