Um seletor parece uma descrição de elementos, e na maior parte do tempo é assim que se escreve um: nav ul li a lê-se como "os links nos itens de lista nas listas da navegação". A especificação não o lê assim. O Selectors Level 4 (W3C Working Draft, 22 de janeiro de 2026) define um seletor pelo que ele casa, e define o casamento como um algoritmo que começa na ponta direita e volta para a esquerda. Quando isso fica claro, a maioria das surpresas no comportamento dos seletores deixa de ser surpresa: por que table > tr não casa com nada numa tabela escrita sem tbody, por que [type=CHECKBOX] casa com um type="checkbox" em minúsculas enquanto .LEAD não casa com class="lead", por que :is(:unknown, p) funciona num navegador que nunca ouviu falar de :unknown, e por que [unclosed é um seletor válido. O testador de seletores CSS roda esse algoritmo sobre qualquer HTML que você cole, na página, e marca os casamentos de cada seletor na árvore que o parser construiu. Este artigo é a leitura da especificação por trás dele. A especificação foi lida em 2026-10-05 e é citada como está.

Da direita para a esquerda, um composto de cada vez

Um seletor complexo é uma cadeia de seletores compostos unidos por combinadores: em nav > ul li:first-child a, os compostos são nav, ul, li:first-child e a, e os combinadores são filho, descendente e descendente. O algoritmo de casamento da especificação (seção 17.3) recebe um elemento e a cadeia e diz: "To match a complex selector against an element, process it compound selector at a time, in right-to-left order." O elemento é verificado primeiro contra o composto mais à direita, a. Se falha, o seletor inteiro falha para esse elemento de imediato. Se passa, o algoritmo procura um elemento relacionado a ele pelo combinador mais à direita (aqui, qualquer ancestral, para o combinador de descendente) que case com o resto da cadeia, e recorre.

Essa ordem é a razão de os seletores serem baratos de avaliar para os navegadores, e de o composto mais à direita ser o que mais decide. nav ul li a não encontra a navegação e desce; encontra cada a e sobe. Quando um seletor é rodado contra um documento inteiro, como faz o querySelectorAll, os candidatos são "the root elements and all of their descendant elements, sorted in shadow-including tree order" (17.5), e o resultado mantém essa ordem do documento, qualquer que seja a ordem em que as partes do seletor foram escritas. O testador mostra os casamentos exatamente nessa ordem, com o caminho de cada elemento e a linha de onde veio.

A linguagem do documento decide o que significa "o mesmo nome"

A seção 3.7 traça a linha: "All Selectors syntax is case-insensitive (i.e. [a-z] and [A-Z] are equivalent), except for the parts that are not under the control of Selectors: specifically, the case-sensitivity of document language element names, attribute names, and attribute values depends on the document language." Nomes de pseudoclasses, palavras-chave como odd e even, as flags i e s: tudo sem distinção de caixa. Nomes de elementos, nomes de atributos e valores de atributos: o que a linguagem do documento disser.

Para documentos HTML, o HTML Standard diz na sua seção "Case-sensitivity of selectors": "When comparing a CSS element type selector to the names of HTML elements in HTML documents, the CSS element type selector must first be converted to ASCII lowercase", e o mesmo para "the name part of the CSS attribute selector". Assim P.LEAD encontra cada p, porque P é passado para minúsculas antes da comparação. A parte da classe é outra história: valores de classe e de id distinguem caixa, então .LEAD não casa com class="lead". (O Quirks Mode Standard definiu um dia um quirk que tornava os seletores de classe e id insensíveis a caixa em documentos em quirks mode; o standard atual, lido em 2026-10-05, não o tem mais.)

Valores de atributo distinguem caixa por padrão, com uma lista de exceções. O HTML Standard nomeia 46 atributos cujos valores um seletor de atributo "must treat ... as ASCII case-insensitive", entre eles type, lang, dir, rel, media, method, target, checked, disabled e readonly. É por isso que input[type=CHECKBOX] casa com type="checkbox", enquanto [title=HELLO] não casa com title="Hello". O Selectors 4 acrescenta duas flags para sobrepor o padrão (6.3): [title="hello" i] compara sem distinção de caixa seja o que for que a linguagem diga, e [type="checkbox" s] compara com distinção de caixa mesmo para um dos 46; o HTML Standard dá exatamente esse exemplo, "The selector [type=a s] will match any HTML element with a type attribute whose value is a, but not whose value is A". A flag s está na especificação e ainda não no Chromium (verificado em 2026-10-05); o testador a aceita e diz isso.

Os escapes seguem o CSS: "the selector #foo>a contains a combinator, but #foo>a instead selects an element with the id foo>a" (3.7). A forma hexadecimal também funciona, com um detalhe do CSS Syntax Level 3 (4.3.7): um escape hexadecimal consome um caractere de espaço depois dele, então #foo\3e a ainda é o identificador único foo>a, e só #foo\3e a, com dois espaços, é um id seguido de um seletor de tipo.

Atributos: sete operadores e a string vazia

A seção 6 define sete seletores de atributo. [att] é presença. [att=val] é igualdade exata. [att~=val] trata o valor como "a whitespace-separated list of words, one of which is exactly val", que é o que um seletor de classe é: a seção 6.6 diz que em HTML o seletor de classe "is equivalent to the = notation applied to the local class attribute (i.e. [class=identifier])". [att|=val] casa com um valor "either being exactly val or beginning with val immediately followed by - (U+002D)", escrito para códigos de língua ([lang|=pt] casa com pt e pt-BR). [att^=val], [att$=val] e [att*=val] são começa-com, termina-com e contém.

Quatro deles compartilham uma regra que derruba quem monta um valor dinamicamente: a string vazia não casa com nada. Para ~=, "if val is the empty string, it will never represent anything", e a mesma frase se repete para os três operadores de substring (6.2). [class~=""] e [href^=""] são seletores válidos que não casam com elemento algum, o que não é o que um programador espera de "começa com a string vazia".

A contagem de irmãos

As pseudoclasses estruturais de árvore (seção 13) contam filhos. :nth-child(An+B) "represents elements that are among An+Bth elements from the list composed of their inclusive siblings", com índice a partir de 1; 2n+1 e odd são o primeiro, o terceiro, o quinto; -n+3 são os três primeiros; n+2 é tudo a partir do segundo. O Level 4 acrescenta a forma of S: :nth-child(2 of .k) é o segundo irmão entre os que casam com .k, o que não é .k:nth-child(2), o segundo irmão se ele também for .k. A especificação explicita o padrão: "If S is omitted, it defaults to |". As formas tipadas (:nth-of-type(), :first-of-type, :only-of-type) contam só os irmãos do mesmo tipo de elemento e não aceitam of S.

Os combinadores (seção 14) são definidos só em termos de elementos. Para o combinador de irmão seguinte +: "The elements represented by the two compound selectors share the same parent in the document tree and the element represented by the first compound selector immediately precedes the element represented by the second one. Non-element nodes (e.g. text between elements) are ignored when considering the adjacency of elements." Texto, comentários e espaços entre dois elementos não quebram h1 + p, e também não contam como filhos para :first-child. O que muda a contagem é a árvore que o parser de HTML constrói em vez da que você digitou: table > tr não casa com nada num documento escrito sem tbody, porque o parser inseriu um, e o explicador de estrutura HTML mostra essa inserção. O testador roda o mesmo parser, então as suas contagens são as contagens do navegador.

As pseudoclasses lógicas, e a lista que perdoa

A seção 4 define quatro pseudoclasses que recebem seletores como argumento. :is() casa com um elemento que qualquer dos seus argumentos case; :where() é a mesma coisa, "with the same syntax and functionality as :is()", exceto que "its specificity is always zero" (4.4); :not() "represents an element that is not represented by its argument" (4.3); :has() "represents an element if any of the relative selectors would match at least one element when anchored against this element" (4.5), que é o tão pedido seletor de pai: li:has(> a[href]) é um item de lista com um link filho, section:not(:has(h1, h2, h3)) uma seção sem título. Dois limites são declarados sem rodeios: ":has() is not valid within :has()", e pseudoelementos não são válidos dentro de nenhuma das quatro.

A diferença entre :is() e :not() que mais importa não é o que casam, mas o que fazem com um argumento que não analisa. A regra padrão para uma lista de seletores é dura: "a selector list containing an invalid selector is invalid" (3.9), então uma pseudoclasse desconhecida em a:hover, a:unknown mata a regra inteira. A seção 16.1 explica o custo: "if any selector in the list fails to parse (because it uses new or UA-specific selector features, for instance), the entire selector list becomes invalid. This can make it hard to write CSS that uses new selectors and still works correctly in older user agents." O remédio é a lista de seletores tolerante, que "instead parses each selector in the list individually, simply ignoring ones that fail to parse, so the remaining selectors can still be used", e que é "used in :is() and :where() only". Assim :is(:unknown, p) é válido e casa com cada p, :not(:unknown, p) é inválido e não casa com nada, e :is(:unknown) é um seletor válido cuja lista está vazia: "If the argument, after parsing, is an empty list, the pseudo-class is valid but matches nothing" (4.2). Envolver o seletor de uma regra em :is() é uma forma documentada de torná-lo tolerante.

A especificidade segue a mesma estrutura (seção 15): ids contam na primeira posição, classes, atributos e pseudoclasses na segunda, tipos e pseudoelementos na terceira, e o seletor universal não conta nada. As quatro pseudoclasses lógicas são a exceção que torna a regra interessante: :is(), :not() e :has() tomam "the specificity of the most specific complex selector in its selector list argument", :where() é "replaced by zero", e :nth-child(An+B of S) é uma pseudoclasse mais o membro mais específico de S. Os exemplos da própria especificação são o melhor teste de uma implementação: :is(em, #foo) é (1,0,0), .qux:where(em, #foo#bar#baz) é (0,1,0), :nth-child(even of li, .item) é (0,2,0), :not(em, strong#foo) é (1,0,1). Membros que uma lista tolerante descartou "must be treated as having zero specificity" (16.1). O testador conta a especificidade a partir do seletor analisado, então esses saem como a especificação diz; a calculadora de especificidade CSS conta o texto como escrito e classifica vários seletores entre si.

O que um documento estático não pode dizer

Algumas pseudoclasses descrevem o usuário, não o documento. Para :hover, :active, :focus, :focus-visible e :focus-within, a seção 9 é explícita: "In non-interactive user agents, these pseudo-classes are valid, but never match any element." Uma ferramenta que lê HTML sem renderizá-lo é um agente de usuário não interativo, e também o é um servidor renderizando uma página ou um teste que analisa um fixture; tal ferramenta responde "válido, zero casamentos", e deve dizer por quê em vez de deixar o zero ser mal lido. :visited vem "with obvious privacy implications", e a primeira das implementações que a seção 8.2 permite é "Have :visited never match, so all links match :link instead"; o testador faz exatamente isso. :target, os estados de mídia e as pseudoclasses de shadow tree também não têm sentido numa árvore estática.

As pseudoclasses de formulário dividem-se em duas. :checked, :disabled, :enabled, :required, :optional, :read-only, :read-write e :open são definidas pelo HTML Standard em termos de atributos e ancestralidade, então podem ser lidas de uma árvore estática: um fieldset desabilitado "causes all the form control descendants of the fieldset element, excluding those that are descendants of the fieldset element's first legend element child, if any, to be disabled", o que o testador respeita. :valid, :invalid, :in-range, :out-of-range, :indeterminate, :default e :placeholder-shown dependem de validação de restrições e de estado que um navegador calcula; o testador não as avalia e diz isso, e é por isso que input:valid é o único seletor em que ele e o Chromium divergem nos vetores entregues por desenho.

:empty é um caso em que a especificação se moveu e o motor de navegador verificado aqui ainda não a seguiu. O Level 4 diz que um elemento é vazio quando "has no children except, optionally, document white space characters", e a nota explica a mudança em relação ao Level 3, em que espaços contavam como conteúdo: "given white space is largely collapsible in HTML and is therefore used for source code formatting ... elements which authors perceive of as empty can be selected". O Chromium, verificado em 2026-10-05, ainda aplica a regra do Level 3, então <p> </p> não casa com p:empty no navegador. O testador segue o navegador e anexa uma nota, porque uma ferramenta que discorda dos navegadores num comportamento visível ensina a lição errada mesmo quando concorda com o rascunho.

Um seletor que termina num pseudoelemento (p::first-line, li::before) nomeia uma caixa, não um elemento; a seção 3.6 diz que um pseudoelemento "represents" o pseudoelemento, e o querySelectorAll não devolve nada para tal seletor, por desenho. O testador, em vez disso, lista os elementos que originariam o pseudoelemento e rotula o seletor, que é a pergunta que um autor de estilos está de fato fazendo.

Inválido, e o que os navegadores fazem com um seletor inacabado

A seção 3.9 é curta e total: "An invalid selector represents, and therefore matches, nothing." Inválido significa um erro de análise ("an unrecognized token or a token which is not allowed at the current parsing point"), um prefixo de namespace não declarado, um seletor vazio numa lista e, pela nota que mantém o CSS extensível, qualquer coisa que um agente de usuário não suporte: os UAs "must treat as invalid any pseudo-classes, pseudo-elements, combinators, or other syntactic constructs for which they have no usable level of support". Essa última cláusula é a razão de um navegador sem suporte a :has() ter descartado em silêncio cada regra que o usava, e de a lista tolerante existir.

Um caso que as pessoas esperam que seja inválido não é. [unclosed, :is( e a[href^="/d analisam todos. O CSS Syntax Level 3 (Candidate Recommendation Draft, 1 de outubro de 2026) diz o que acontece quando a entrada termina dentro de um bloco ou de uma função: para um bloco simples, em <eof-token>, "Discard a token from input. Return block." (5.5.9), e o mesmo para uma função (5.5.10); para uma string, o fim da entrada "is a parse error" e o token de string é devolvido mesmo assim (4.3.5). A explicação da própria especificação, na sua visão geral não normativa, é que "by the time the parser is finished, the presence of the ending token is no longer possible to determine; all you have is the fact that there's a block and a function". Assim [unclosed é [unclosed], :is( é :is() (válido, não casa com nada), e a[href^="/d é a[href^="/d"]. O querySelectorAll do Chromium concorda em cada um deles. O testador os lê do mesmo modo e anexa uma nota dizendo onde a entrada acabou, porque um seletor que funciona por acidente num lugar falhará em outro no momento em que algo o seguir.

Fontes