:has(): o Seletor que 'Sobe' pro Pai
2 min de leitura
Por anos, CSS foi estritamente unidirecional: o seletor olha
pra filho ou irmão, nunca pro pai. Você sempre precisou
de JavaScript pra "se o pai tem X, faça Y" - tipo "se a <form>
tem campo inválido, mude a cor do botão de submit".
:has() mudou isso. É o seletor pai: :has(X) significa
"elemento que tem X como descendente". Suporte amplo em
navegadores modernos (Chrome 105+, Safari 15.4+, Firefox 121+).
Exemplo clássico: form com erro
<form class="formulario">
<input type="email" required value="invalido" />
<input type="text" />
<button>Enviar</button>
</form>
/* Se o form tem input inválido, o botão fica desabilitado visualmente */
.formulario:has(input:invalid) button {
background: #ccc;
cursor: not-allowed;
}
Sem JavaScript, sem classe extra, sem mexer no HTML. O CSS "percebe" que existe um input inválido dentro do form e estiliza o botão de acordo.
Card com imagem vs sem
<div class="card">
<img src="foto.jpg" alt="" />
<h3>Título</h3>
<p>Descrição</p>
</div>
<div class="card">
<h3>Só texto</h3>
<p>Esse card não tem imagem</p>
</div>
/* Cards com imagem ocupam mais espaço vertical */
.card:has(img) {
grid-template-rows: 200px auto auto;
}
/* Cards sem imagem são mais compactos */
.card:not(:has(img)) {
grid-template-rows: auto auto;
}
Layout que muda baseado em irmãos
<article>
<h1>Título</h1>
<img src="capa.jpg" alt="" />
<p>Texto do artigo...</p>
</article>
/* Se tem imagem, o h1 vai em cima e a imagem em baixo com tamanho cheio */
article:has(img) h1 {
margin-bottom: 16px;
}
Estilizar inputs baseado no estado deles
/* Label de campo obrigatório fica vermelha se o campo está vazio e
teve foco e perdeu o foco sem preencher (touched) */
.campo:has(input:user-invalid) .label {
color: #d93025;
}
:user-invalid é uma pseudo-classe moderna (ainda em adoção) que
diferencia "inválido por causa de validação nativa" de "inválido
porque o usuário digitou errado e perdeu o foco". Combinar com
:has() cobre o caso sem JavaScript.
Layout condicional a partir do conteúdo
<section class="lista-de-posts">
<article>...</article>
<article>...</article>
<article>...</article>
</section>
<section class="lista-de-posts">
<!-- vazia -->
</section>
/* Se a lista tem posts, layout em grid */
.lista-de-posts:has(article) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}
/* Se está vazia, mostra mensagem centralizada */
.lista-de-posts:not(:has(article))::before {
content: "Nenhum post ainda";
text-align: center;
color: #999;
display: block;
padding: 48px;
}
O ::before aparece só quando a lista está vazia. Puro CSS.
:has() dentro de :has() - aninhamento
/* Card que tem um botão desabilitado dentro */
.card:has(button:disabled) {
opacity: 0.6;
}
Você pode aninhar quantos precisar. Cada nível é uma seleção adicional.
:has(*:not(...)) - "tem algo que não é X"
/* Header que tem APENAS logo (sem menu) */
.header:not(:has(nav)) {
justify-content: center; /* centraliza o logo */
}
Limitações e cuidado
Performance: :has() é mais "caro" que seletores simples, porque
o navegador precisa olhar os descendentes de cada candidato.
Em listas enormes (milhares de itens) com :has() em cada um,
pode haver lentidão. Na prática, em 99% dos casos, é imperceptível.
Suporte: navegadores antigos (pré-2021) não entendem. Para projetos que precisam suportar IE ou Safari muito velho, ter fallback é importante.
:has(X)seleciona o elemento que tem X como descendente.- Combina com tudo:
:has(),:not(:has()),:has(:has()). - Substitui muito JavaScript: validação de form, layout condicional, estados de UI.
- Funciona com pseudo-classes (
:invalid,:checked,:user-invalid). - Performance boa em geral; aninhar muito é o único cuidado.
Dica: pense em
:has()como "if em CSS". É a primeira vez que o CSS tem condicional real ("se este elemento tem X, então Y"). É tão transformador quanto Flexbox foi em 2010.
No próximo nó, vamos falar de logical properties - a forma moderna de CSS que respeita a direção de escrita e deixa seu código pronto para internacionalização.