Você já viu :where() e :is() em algum código CSS moderno. Talvez tenha até consultado a MDN. "São pseudo-classes para agrupar seletores", certo?

Aí você tenta usar e... não entende quando cada um faz diferença. A sintaxe funciona, mas o comportamento às vezes é inesperado. Você acaba voltando pros seletores tradicionais porque "pelo menos funcionam".

Durante meses, ignorei esses seletores completamente. Achava que eram apenas "syntax sugar" sem benefício real. Até o dia que meu CSS escalou pra mais de 50 componentes e virou um inferno de !important e conflitos de especificidade.

Foi aí que esses seletores finalmente fizeram sentido.


O Problema Invisível Que Você Só Vê Quando Dói

Especificidade em CSS é silenciosa. Você não vê, não debugga facilmente, e só percebe quando já está no meio do caos.

Pensa nesse cenário super comum:

/* Você cria estilos base para headings */
.card h2,
.panel h2,
.modal h2 {
    font-size: 1.5rem;
    color: #333;
}

/* Semanas depois, precisa mudar SÓ o modal */
.modal h2 {
    font-size: 1.2rem; /* Por que não funciona??? */
}

Ambos têm especificidade (0,0,2,0). Empate. O último deveria ganhar, mas... e se tem outros seletores no meio? E se a ordem dos arquivos muda? E se tem CSS de biblioteca externa?

Solução clássica: aumentar especificidade.

.modal .header h2 { /* Agora sim! */
    font-size: 1.2rem;
}

/* Até precisar mudar de novo... */
.modal .header.compact h2 { /* Mais especificidade! */
    font-size: 1.1rem;
}

/* E de novo... */
.modal .header.compact.dark h2 { /* WTF */
    font-size: 1.1rem !important; /* Desisto */
}

Esse ciclo vicioso é o verdadeiro problema que :where() e :is() resolvem.


A Diferença Que Ninguém Explica Direito

:where() tem especificidade ZERO.
:is() mantém a especificidade do seletor mais específico dentro dele.

Parece detalhe técnico? É a diferença entre CSS escalável e inferno de manutenção.

Especificidade em números

Quando você escreve .card h2:

  • Especificidade: (0,0,2,0) - uma classe + um elemento

Quando escreve :where(.card) h2:

  • Especificidade: (0,0,0,1) - :where() conta ZERO + um elemento

Quando escreve :is(.card, #container) h2:

  • Especificidade: (0,1,0,1) - o #container (mais específico) + um elemento

Percebe? O conteúdo dentro de :where() é completamente ignorado no cálculo de especificidade.


Exemplo Real: Reset CSS Que Não Quebra Tudo

Sabe aqueles resets CSS que você copia e depois precisa sobrescrever com !important? :where() resolve isso.

/* Reset tradicional - especificidade normal */
h1, h2, h3, h4, h5, h6 {
    margin: 0;
    padding: 0;
    font-weight: normal;
}

/* Agora tenta sobrescrever em um componente */
.article h2 {
    margin-bottom: 1rem; /* Funciona, mas... */
}

/* Problema aparece aqui */
.card h2 {
    margin-bottom: 0.5rem; /* Também funciona */
}

/* Mas e se você combinar? */
.article .card h2 {
    /* Qual margin ganha? Depende da ordem no CSS! */
}

Agora com :where():

/* Reset com especificidade ZERO */
:where(h1, h2, h3, h4, h5, h6) {
    margin: 0;
    padding: 0;
    font-weight: normal;
}

/* QUALQUER seletor sobrescreve facilmente */
.article h2 {
    margin-bottom: 1rem; /* Ganha sempre */
}

h2 {
    margin-bottom: 2rem; /* Até isso ganha! */
}

/* Sem guerra de especificidade */
.article .card h2 {
    margin-bottom: 0.75rem; /* Previsível */
}

Repare: o reset existe como "piso", não como obstáculo.


Caso Real: Componentes Com Estados

Imagine botões que precisam de hover, focus, active, disabled... em múltiplos contextos.

Abordagem antiga (pesadelo de manutenção)

/* Você escreve isso... */
.btn:hover,
.btn:focus,
.btn:active,
.link:hover,
.link:focus,
.link:active,
.nav-item:hover,
.nav-item:focus,
.nav-item:active {
    color: blue;
    border-color: blue;
}

/* E quando adiciona novo estado? */
.btn:hover,
.btn:focus,
.btn:active,
.btn:disabled, /* +1 linha */
.link:hover,
.link:focus,
.link:active,
.link:disabled, /* +1 linha */
.nav-item:hover,
.nav-item:focus,
.nav-item:active,
.nav-item:disabled { /* +1 linha */
    /* ... */
}

/* 12 linhas para 3 componentes × 4 estados = insano */

Com :is() (legível e mantível)

/* Uma linha. Fim. */
:is(.btn, .link, .nav-item):is(:hover, :focus, :active, :disabled) {
    color: blue;
    border-color: blue;
}

/* Adicionar novo componente? */
:is(.btn, .link, .nav-item, .badge):is(:hover, :focus, :active, :disabled) {
    /* Literalmente adicionar palavra na lista */
}

Por que isso funciona?

:is(.btn, .link, .nav-item) tem especificidade da classe mais específica: (0,0,1,0).

Todos os componentes ali dentro ficam com a mesma especificidade, facilitando override quando necessário.


Erro Clássico: Confundir Os Dois

Durante semanas, eu usava :is() pra tudo. "Se funciona, por que complicar?"

/* Eu fazia isso */
:is(.card, .panel, .modal) h2 {
    font-size: 1.5rem;
}

/* E depois ficava confuso porque era difícil sobrescrever */
.modal h2 {
    font-size: 1.2rem; /* Às vezes funcionava, às vezes não */
}

O que acontece:

:is(.card, .panel, .modal) h2 tem especificidade (0,0,1,1) - igual a .modal h2.

Quando tem empate, a ordem no CSS importa. Se o :is() vem depois, ele ganha. Imprevisível.

Solução

Use :where() quando quer criar base facilmente sobrescrevível:

/* Base com especificidade zero */
:where(.card, .panel, .modal) h2 {
    font-size: 1.5rem;
}

/* Sobrescreve sem drama */
.modal h2 {
    font-size: 1.2rem; /* Sempre funciona */
}

Use :is() quando quer agrupar mantendo lógica de especificidade:

/* Agrupar estados - especificidade controlada */
.card:is(:hover, :focus) {
    border-color: blue;
}

Design System Real: Camadas de Especificidade

Aqui está um padrão que uso em todo projeto agora:

/* CAMADA 1: Reset universal (especificidade 0) */
:where(button, input, select, textarea) {
    font-family: inherit;
    font-size: 100%;
    margin: 0;
}

/* CAMADA 2: Estilos base de componentes */
.btn {
    padding: 0.5rem 1rem;
    border-radius: 4px;
    background: var(--primary);
}

/* CAMADA 3: Variantes com :is() */
.btn:is(.small, .compact) {
    padding: 0.25rem 0.5rem;
}

/* CAMADA 4: Contextos específicos */
:is(.header, .footer) .btn {
    padding: 0.375rem 0.75rem;
}

/* CAMADA 5: Estados */
.btn:is(:hover, :focus-visible) {
    background: var(--primary-dark);
}

Cada camada tem especificidade crescente de forma previsível. Nada de surpresas.


Quando Usar Cada Um

✅ Use :where() para:

Resets e bases universais

:where(h1, h2, h3, h4, h5, h6) {
    line-height: 1.2;
}

Estilos padrão que você QUER que sejam sobrescritos

:where(.card, .panel) {
    padding: 1rem;
    border: 1px solid #ddd;
}

Design tokens aplicados amplamente

:where(.btn, .link, .badge) {
    transition: all 0.2s ease;
}

✅ Use :is() para:

Agrupar estados mantendo especificidade

.input:is(:hover, :focus, :active) {
    border-color: var(--accent);
}

Simplificar seletores complexos

/* Antes */
.sidebar a:hover,
.sidebar a:focus,
.footer a:hover,
.footer a:focus {
    text-decoration: underline;
}

/* Depois */
:is(.sidebar, .footer) a:is(:hover, :focus) {
    text-decoration: underline;
}

Criar variantes de componentes

.btn:is(.primary, .success, .danger) {
    color: white;
}

❌ Evite usar quando:

Um seletor simples já resolve

/* Não precisa */
:where(.btn) {
    background: blue;
}

/* Melhor */
.btn {
    background: blue;
}

Você quer alta especificidade de propósito

/* Se precisa garantir que isso NÃO seja sobrescrito, não use :where() */
.modal.critical {
    z-index: 9999;
}

Compatibilidade: Você Pode Usar Hoje

:where() e :is() funcionam em:

  • Chrome/Edge 88+

  • Firefox 78+

  • Safari 14+

Isso cobre 95%+ dos usuários em 2024.

Para browsers antigos, você pode usar progressive enhancement:

/* Fallback para browsers sem suporte */
.card,
.panel,
.modal {
    padding: 1rem;
}

/* Sobrescrever com :where() em browsers modernos */
@supports selector(:where(*)) {
    :where(.card, .panel, .modal) {
        padding: 1rem;
    }
    
    /* Limpar fallback */
    .card,
    .panel,
    .modal {
        all: unset;
    }
}

Resumo: O Que Realmente Importa

:where() = especificidade zero = base sobrescrevível

  • Use pra resets

  • Use pra estilos universais

  • Use quando quer facilitar overrides

:is() = agrupa sem mudar comportamento de especificidade

  • Use pra estados

  • Use pra simplificar código

  • Use quando quer legibilidade

Não é sobre decorar sintaxe. É sobre ter controle sobre especificidade ao invés de lutar contra ela.


Próximos Passos

Abra um projeto e procure por:

  1. Resets que você sobrescreve com !important → Refatore com :where()

  2. Listas longas de seletores com vírgula → Simplifique com :is()

  3. Guerras de especificidade → Crie camadas claras com :where() e :is()

Você vai se perguntar como viveu sem isso. Sério.

Dúvidas? Comenta que eu ajudo! 💪