Seletores modernos: :has(), :is(), :where() e nesting

TL;DR

:has() é o “parent selector” que a web não tinha — seleciona um elemento baseado nos seus descendentes ou irmãos, abrindo patterns impossíveis antes. :is() e :where() eliminam seletor repetitivo (mesmo resultado, especificidades diferentes). CSS Nesting elimina o pré-processador para estrutura de seletores. Juntos, esses quatro recursos tornam o CSS mais expressivo sem aumentar a contagem de bytes.


:is() — agrupamento sem repetição

Antes de :is(), agrupar seletores com contextos diferentes exigia repetir os sufixos:

/* ❌ Repetitivo */
.card h1, .card h2, .card h3,
.modal h1, .modal h2, .modal h3,
.article h1, .article h2, .article h3 {
  line-height: 1.25;
}
 
/* ✅ Com :is() */
:is(.card, .modal, .article) :is(h1, h2, h3) {
  line-height: 1.25;
}

A especificidade de :is() é a do seletor mais específico dentro dos parênteses:

:is(h1, h2, .titulo) { color: blue; }
/* Especificidade: (0, 1, 0) — .titulo é o mais específico */
 
:is(#destaque, .card, p) { margin: 0; }
/* Especificidade: (1, 0, 0) — #destaque é o mais específico */

Armadilha de especificidade

Se você misturar IDs com classes em :is(), todos os seletores herdam a especificidade mais alta. Prefira :where() quando quiser especificidade zero, ou mantenha :is() com seletores de mesmo nível de especificidade.


:where() — agrupamento sem especificidade

:where() funciona exatamente como :is() mas tem especificidade zero — sempre:

:where(h1, h2, h3, h4, h5, h6) { line-height: 1.25; }
/* Especificidade: (0, 0, 0) — qualquer outro seletor vence */

Isso torna :where() ideal para resets, estilos base, e bibliotecas de componentes onde você quer que o usuário possa sobrescrever facilmente:

/* Reset que nunca vai entrar em guerra de especificidade */
:where(*, *::before, *::after) {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
 
/* Estilos base que qualquer classe simples pode sobrescrever */
:where(h1, h2, h3, h4, h5, h6) {
  font-weight: 700;
  line-height: 1.25;
}
 
/* O usuário sobrescreve com (0, 1, 0) — especificidade baixíssima, já suficiente */
.section-title { font-weight: 400; }

:is() vs :where() — quando usar cada um

SituaçãoUse
Resets e estilos base de biblioteca:where() — especificidade zero facilita override
Agrupamento de seletores no seu CSS:is() — especificidade do argumento mais alto
Você quer que usuários sobrescrevam facilmente:where()
Você quer consistência de especificidade:is() com seletores do mesmo nível

:not() — exclusão com lógica de lista

:not() aceita lista de seletores (desde Selectors Level 4):

/* Seleciona todos os li exceto o primeiro e o último */
li:not(:first-child, :last-child) {
  border-top: 1px solid var(--color-border);
}
 
/* Links externos (com href que começa com http) */
a:not([href^="/"]):not([href^="#"]) {
  color: var(--color-external);
}
 
/* Input que não é checkbox, radio nem submit */
input:not([type="checkbox"], [type="radio"], [type="submit"]) {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}
 
/* Todo card exceto o destacado */
.card:not(.card--featured) {
  opacity: 0.8;
}

A especificidade de :not() é a do seletor mais específico dentro:

:not(#id) { /* especificidade: (1, 0, 0) */ }
:not(.class) { /* especificidade: (0, 1, 0) */ }
:not(div) { /* especificidade: (0, 0, 1) */ }

:has() — o seletor de pai (e contexto)

:has() é a mudança mais significativa em CSS seletores em uma década. Seleciona um elemento que tem um descendente correspondente ao argumento:

/* Seleciona .card que contém uma img */
.card:has(img) {
  padding: 0;
}
 
/* Seleciona label que contém um input em foco */
label:has(input:focus) {
  color: var(--color-primary);
  font-weight: 600;
}
 
/* Seleciona form com campos inválidos */
form:has(:invalid) {
  border-color: var(--color-danger);
}
 
/* Parágrafo imediatamente após um h2 */
h2 + p { }
/* Mas com :has(), selecionar o h2 que tem um p como próximo irmão */
h2:has(+ p) {
  margin-bottom: 0;
}

Casos de uso que :has() desbloqueia

1. Layout condicional baseado no conteúdo

/* Card sem imagem → padding normal */
.card { padding: 1.5rem; }
 
/* Card com imagem → sem padding no topo (imagem sangra até a borda) */
.card:has(> .card__image:first-child) {
  padding-top: 0;
}
 
/* Grid de 3 colunas quando há 3+ items */
.grid:has(.item:nth-child(3)) {
  grid-template-columns: repeat(3, 1fr);
}

2. Form UX sem JavaScript

/* Label muda quando seu input está preenchido */
.field:has(input:not(:placeholder-shown)) .field__label {
  transform: translateY(-1.5rem) scale(0.85);
  color: var(--color-primary);
}
 
/* Mensagem de erro aparece quando input é inválido E já foi tocado */
.field:has(input:invalid:not(:focus):not(:placeholder-shown)) .field__error {
  display: block;
}
 
/* Botão de submit desabilitado visualmente quando form tem inválidos */
form:has(:invalid) .btn--submit {
  opacity: 0.5;
  cursor: not-allowed;
}

3. Seleção de irmão anterior — impossível antes

/* :has() com seletor de irmão + e ~ */
 
/* li anterior ao li em hover (via pai) */
li:has(+ li:hover) {
  opacity: 0.5; /* o item antes do hovered fica opaco */
}
 
/* Todos os li antes do hovered */
li:has(~ li:hover) {
  opacity: 0.5;
}

4. Dark mode de componente baseado em contexto

/* Nav especial quando o header tem classe .hero */
header:has(.hero) nav {
  background: transparent;
  color: white;
}

:nth-child() com seletor — refinamento

CSS Selectors 4 permite passar um seletor de filtro para :nth-child():

/* O 2° elemento p (ignora outros elementos) */
p:nth-child(2 of p) { }
 
/* O 1° .card — entre todos os filhos, o 1° que é .card */
.card:nth-child(1 of .card) { }
/* Antes: :first-child só funcionava se .card fosse o 1° filho real */
 
/* Todo .card par */
.card:nth-child(even of .card) {
  background: var(--color-bg-raised);
}

CSS Nesting

CSS Nesting (nativo, sem Sass) permite escrever seletores aninhados dentro de regras pai:

/* Sem nesting — repetição de contexto */
.card { border: 1px solid var(--color-border); }
.card:hover { border-color: var(--color-primary); }
.card .card__title { font-size: 1.25rem; }
.card .card__title:first-child { margin-top: 0; }
.card--featured { border-color: var(--color-primary); }
 
/* Com nesting — co-localizado, sem repetição */
.card {
  border: 1px solid var(--color-border);
 
  &:hover {
    border-color: var(--color-primary);
  }
 
  .card__title {
    font-size: 1.25rem;
 
    &:first-child {
      margin-top: 0;
    }
  }
 
  &--featured {                         /* .card--featured */
    border-color: var(--color-primary);
  }
}

Regras de nesting

O & representa o seletor pai. Pode ser omitido em alguns casos, mas é mais seguro e legível incluir:

.parent {
  color: blue;
 
  /* & explícito — recomendado */
  & .child { color: red; }
  &:hover { color: green; }
  &.modifier { color: purple; }
 
  /* Sem & — só funciona com seletores de elemento/classe/id */
  .child { color: red; }          /* equivale a .parent .child */
 
  /* & pode aparecer em qualquer posição */
  .grandparent & { color: orange; } /* .grandparent .parent */
}

Nesting com at-rules

.card {
  padding: 1rem;
 
  /* Media query aninhada */
  @media (width >= 768px) {
    padding: 2rem;
    display: grid;
    grid-template-columns: 150px 1fr;
  }
 
  /* Container query aninhada */
  @container (width >= 400px) {
    flex-direction: row;
  }
 
  /* Layer aninhado */
  @layer overrides {
    &.special { border: 2px solid red; }
  }
}

Nesting BEM — o melhor dos dois mundos

.card {
  /* Block */
  display: flex;
  flex-direction: column;
 
  /* Elements */
  &__header {
    padding: 1rem;
    border-bottom: 1px solid var(--color-border);
  }
 
  &__body {
    flex: 1;
    padding: 1rem;
  }
 
  &__footer {
    padding: 0.75rem 1rem;
    background: var(--color-bg);
  }
 
  /* Modifiers */
  &--featured {
    border: 2px solid var(--color-primary);
  }
 
  &--compact {
    .card__header,
    .card__body,
    .card__footer { padding: 0.5rem; }
  }
 
  /* States */
  &:hover { box-shadow: var(--shadow-md); }
  &:focus-within { outline: 2px solid var(--color-primary); }
}

Combinação: :has() + nesting

/* Formulário que se adapta ao estado dos inputs, todo co-localizado */
.field {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
 
  &__label {
    font-size: var(--text-sm);
    color: var(--color-text-muted);
    transition: color var(--duration-fast);
  }
 
  &__input {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: 0.5rem 0.75rem;
 
    &:focus {
      outline: 2px solid var(--color-primary);
      border-color: var(--color-primary);
    }
 
    &:invalid:not(:focus):not(:placeholder-shown) {
      border-color: var(--color-danger);
    }
  }
 
  &__error {
    display: none;
    font-size: var(--text-xs);
    color: var(--color-danger);
  }
 
  /* :has() no pai reage ao estado do filho */
  &:has(.field__input:focus) .field__label {
    color: var(--color-primary);
  }
 
  &:has(.field__input:invalid:not(:focus):not(:placeholder-shown)) {
    .field__error { display: block; }
    .field__label { color: var(--color-danger); }
  }
}

Suporte e fallbacks

RecursoSuporte
:is() e :where()Todos os browsers modernos (desde 2021)
:has()Chrome 105+, Safari 15.4+, Firefox 121+ (2024)
CSS NestingChrome 112+, Safari 16.5+, Firefox 117+ (2023)
:nth-child(n of selector)Chrome 111+, Safari 15.4+, Firefox 113+

Para :has(), o progressive enhancement é a abordagem correta — estilos sem :has() como base, e refinamentos com :has() que degradam graciosamente:

/* Base: funciona em todos os browsers */
.card { padding: 1.5rem; }
 
/* Refinamento com :has() — browsers sem suporte ignoram */
@supports selector(:has(img)) {
  .card:has(> img:first-child) { padding-top: 0; }
}

Para fixar

  1. Qual a diferença de especificidade entre :is(.card, #destaque) e :where(.card, #destaque)? O que isso implica na prática?
  2. O que :has() permite fazer que era impossível em CSS antes? Dê dois exemplos concretos.
  3. Como você seleciona o label imediatamente antes de um input:focus usando só CSS (sem JavaScript)?
  4. O que &--modifier dentro de .block { } gera? Qual seletor final?
  5. Como li:has(+ li:hover) funciona? O que + faz dentro de :has()?
  6. Quando você usaria :nth-child(2 of .card) em vez de .card:nth-child(2)? Qual é a diferença?

Veja também