Especificidade no CSS: por que a sua regra não vence
Como o navegador conta o peso de cada seletor em três colunas, o que !important realmente faz e como sair da guerra de especificidade sem gambiarra.
O navegador não aplica a última regra que você escreveu. Ele dá a cada seletor uma pontuação de três números, compara coluna por coluna e aplica a regra que ganhou. A ordem no arquivo só decide quando a pontuação empata.
Todos os exemplos deste artigo saem da mesma página: o boletim on-line da Escola Aurora. Este é o trecho de HTML que vai aparecer o tempo inteiro.
<section id="painel-matricula" class="painel">
<article class="card-aluno destaque">
<span class="nota reprovado">4,5</span>
</article>
</section>Se você ainda está reconhecendo classes, IDs e atributos no markup, revise tags e atributos HTML e volte aqui depois.
Você pode traduzir especificidade como o peso do endereço usado para chegar ao elemento. A gente vai manter o nome técnico, mas sempre conferir esse peso no estilo calculado do navegador, onde a disputa deixa de ser opinião.
O crachá que cada seletor leva para a disputa
Pense na entrada de uma escola: dizer “sou aluno” identifica muita gente, dizer
“sou da turma 3A” reduz o grupo, e mostrar uma carteirinha com matrícula aponta
para uma pessoa. No CSS, a tag é o grupo amplo, a classe é a turma e o id é a
identificação individual. O navegador apenas conta essas categorias e compara
da mais forte para a mais fraca.
Faça uma previsão antes do primeiro teste: olhe os seletores, anote a tripla de cada um e escolha o vencedor sem abrir o navegador. Depois compare sua escolha com a cor calculada no DevTools. Se divergir, a coluna onde você contou errado mostra qual peça do seletor precisa ser revista.
Toda a especificidade cabe numa tripla (a, b, c). Cada pedaço do seletor
soma um ponto em exatamente uma das colunas:
| coluna | o que soma nela | exemplo |
|---|---|---|
| a | cada #id |
#painel-matricula |
| b | cada classe, atributo e pseudo-classe | .nota, [data-turno], :hover |
| c | cada tipo (tag) e pseudo-elemento | span, article, ::before |
O que não pontua: o *, os combinadores >, +, ~ e o espaço, e o
:where(). Eles selecionam, mas valem zero.
span { color: crimson; } /* 0,0,1 */
.nota { color: crimson; } /* 0,1,0 */
#painel-matricula { color: crimson; } /* 1,0,0 */
.card-aluno .nota { color: crimson; } /* 0,2,0 */
#painel-matricula .card-aluno .nota { color: crimson; } /* 1,2,0 */Repare que a conta é por ocorrência, não por tipo de peça. .nota.nota.nota
é um seletor esquisito, mas legítimo, e vale três classes.
Uma calculadora para não contar no dedo
Contar de cabeça funciona até o seletor ficar comprido. O ecossistema do
PostCSS mantém um pacote que faz a conta seguindo a especificação: o
@csstools/selector-specificity. Uma dúzia de linhas transforma ele numa
calculadora de linha de comando.
import { selectorSpecificity } from '@csstools/selector-specificity';
import parser from 'postcss-selector-parser';
const processor = parser();
export function esp(seletor) {
const raiz = processor.astSync(seletor);
const { a, b, c } = selectorSpecificity(raiz.nodes[0]);
return `${a},${b},${c}`;
}
const seletores = process.argv.slice(2);
for (const s of seletores) {
console.log(`${esp(s).padEnd(7)} ${s}`);
}Agora dá para jogar os seletores do boletim nela e ver o placar de cada um:
node calc.mjs "span" ".nota" "#painel-matricula" ".card-aluno .nota" \
"article.card-aluno span.nota" ".nota:hover" ".nota::before" \
"#painel-matricula .card-aluno .nota" "*" ".nota.nota.nota"Duas linhas merecem atenção. .nota:hover vale 0,2,0: a pseudo-classe entrou
na coluna do meio, como se fosse mais uma classe. Já .nota::before vale
0,1,1, porque pseudo-elemento é tipo, não classe. Um : e dois :
pontuam em colunas diferentes, e é por isso que a distinção entre pseudo-classe
e pseudo-elemento importa mais do que parece.
A conta não é em base 10
Este é o mal-entendido que mais custa tempo. Muita gente aprendeu que id vale 100, classe vale 10 e tag vale 1, soma tudo e compara o total. Se fosse assim, onze classes (110) ganhariam de um id (100). Dá para medir isso no Chrome.
Carregue o painel do boletim com onze classes de estado, todas ao mesmo tempo, sem tirar o id dele:
<section id="painel-matricula"
class="painel matricula turma-3a manha ativo destaque compacto com-borda tema-claro imprimivel rolavel"></section>Agora duas regras disputam o mesmo elemento: uma amarra as onze classes de uma vez, a outra usa só o id.
.painel.matricula.turma-3a.manha.ativo.destaque.compacto.com-borda.tema-claro.imprimivel.rolavel {
--vencedor: "as onze classes, 0,11,0";
}
#painel-matricula {
--vencedor: "o id sozinho, 1,0,0";
}Para saber qual das duas pegou, cada regra escreve o próprio nome numa variável
CSS chamada --vencedor. Custom property passa pela cascata exatamente como
color ou margin, então o valor que sobrar em getComputedStyle é,
literalmente, o nome da regra vencedora. Essa leitura do estilo calculado também
aparece em como alterar CSS com JavaScript.
O script abre o Chrome com o Playwright, monta a página e lê a variável:
import { chromium } from 'playwright-core';
const navegador = await chromium.launch();
const pagina = await navegador.newPage();
async function vencedor(css, html, alvo) {
await pagina.setContent(
`<!doctype html><html><head><style>${css}</style></head><body>${html}</body></html>`,
);
return pagina.evaluate(
(s) => getComputedStyle(document.querySelector(s)).getPropertyValue('--vencedor').trim(),
alvo,
);
}[1] onze classes x um id vence: “o id sozinho, 1,0,0”
O id ganhou. A comparação é coluna por coluna, da esquerda para a direita,
e a primeira diferença encerra o assunto: 1 é maior que 0 na coluna a, e
as onze classes nem chegam a ser olhadas. Não existe número de classes que
alcance um id.
:is(), :not() e :where(): onde a conta surpreende
Estas três pseudo-classes recebem outros seletores como argumento, e cada uma trata esse argumento de um jeito. Passando pela calculadora:
node calc.mjs ":is(#painel-matricula, .painel) .nota" \
":where(#painel-matricula, .painel) .nota" \
".nota:not(.aprovado)" ".nota:not(#painel-matricula)" \
".card-aluno:has(.reprovado)"A regra é curta: :is(), :not() e :has() assumem a
especificidade do argumento mais pesado da lista. :where() é a exceção
que existe para isso: ela sempre vale zero, não importa o que você coloque
dentro.
Isso explica dois resultados que costumam pegar gente. :is(#painel-matricula, .painel) .nota casa com a nota tanto de quem tem o id quanto de quem tem só a
classe .painel — mas cobra o preço do id nos dois casos, mesmo quando foi a
classe que casou. E .nota:not(#painel-matricula), um seletor cujo trabalho é
justamente excluir o id, carrega o peso dele mesmo assim.
O Chrome confirma as três pontas. Primeiro, o :where() zerando de verdade: um
id dentro dele perde para duas classes.
:where(#painel-matricula) .nota { --vencedor: ":where(#id) .nota, 0,1,0"; }
.card-aluno .nota { --vencedor: ".card-aluno .nota, 0,2,0"; }Depois, o :is() cobrando o preço do argumento mais caro, e por isso ganhando
de duas classes mesmo estando escrito antes:
.card-aluno .nota { --vencedor: ".card-aluno .nota, 0,2,0"; }
:is(#painel-matricula, .painel) .nota { --vencedor: ":is(#id, .painel) .nota, 1,1,0"; }Por fim, o :not() cobrando o mesmo preço cheio: com um id dentro, ele também
ganha de duas classes mesmo estando escrito antes.
.nota:not(#painel-matricula) { --vencedor: ".nota:not(#id), 1,1,0"; }
.nota:not(.aprovado) { --vencedor: ".nota:not(.aprovado), 0,2,0"; }Empate: fica quem chegou por último
Quando duas regras têm a mesma pontuação, na mesma camada e na mesma origem, o
critério final é a posição no documento. Vence a que o navegador leu por
último — considerando a ordem das folhas de estilo no <head> e, dentro de
cada folha, a ordem das linhas.
.reprovado { --vencedor: ".reprovado, escrita na linha 1"; }
.nota { --vencedor: ".nota, escrita na linha 2"; }Repare no que não importa: a ordem das classes no atributo class do HTML.
O elemento é class="nota reprovado", e ainda assim .nota ganhou, porque
está depois no CSS. O HTML não tem voto nenhum nessa disputa.
!important não é mais forte, ele joga outro campeonato
!important não soma pontos ao seletor. Ele tira a declaração da disputa de
especificidade e move para um patamar acima, onde só existem outras
declarações com !important. Por isso uma classe consegue derrubar um id:
#painel-matricula .nota { --vencedor: "#painel-matricula .nota, 1,1,0"; }
.nota { --vencedor: ".nota com !important, 0,1,0" !important; }Funcionou — e é exatamente aí que mora o problema. A partir do momento em que
o !important entrou, a especificidade volta a valer, só que agora entre os
importantes. Se amanhã alguém precisar sobrescrever essa nota, o único jeito
é outro !important com pontuação maior. O arquivo entra numa escalada em que
cada rodada custa mais que a anterior, e não existe volta sem refatoração.
A ordem completa que o navegador usa para decidir é esta, de cima para baixo. Só se um critério empatar é que o de baixo é consultado:
Especificidade é o quarto critério, não o primeiro — e as duas seções seguintes
tratam justamente dos degraus 2 e 3, que passam na frente dela. É por isso que
discutir seletor não resolve um conflito causado por !important: a disputa nem
chegou nesse degrau.
O atributo style ganha de qualquer seletor
Estilo escrito direto no HTML, no atributo style, não tem seletor e por isso
não tem pontuação. Ele entra na cascata acima de todas as regras normais da sua
folha, inclusive das que usam id:
<section id="painel-matricula" style='--vencedor: "o atributo style do HTML"'></section>#painel-matricula { --vencedor: "#painel-matricula na folha"; }Uma única coisa derruba o style: um !important na folha.
#painel-matricula { --vencedor: "#painel-matricula com !important" !important; }Esse é o motivo real pelo qual tanta gente acaba com !important espalhado:
alguma biblioteca escreveu no style do elemento, e não sobrou outra saída.
A correção durável é impedir a escrita inline, não vencer ela. Para acompanhar
como uma folha externa é ligada ao documento, veja
os recursos carregados pelo head.
@layer: sair da disputa em vez de tentar vencê-la
@layer cria camadas ordenadas dentro da sua folha. A comparação de camada
acontece antes da especificidade, e isso muda o jogo: dentro de uma camada,
a pontuação decide; entre camadas diferentes, quem decide é a ordem das
camadas.
Primeiro, o detalhe que surpreende quem começa: CSS fora de qualquer camada ganha de CSS dentro de camada, por mais pesado que ele seja.
@layer base {
#painel-matricula .nota { --vencedor: "#id .nota, dentro de @layer base"; }
}
.nota { --vencedor: ".nota, fora de qualquer layer"; }Um seletor 1,1,0 perdeu para um 0,1,0. Não tem nada de errado: o código sem
camada é tratado como a camada mais alta de todas.
Agora com as duas regras dentro de camadas declaradas na ordem
reset, base, tema:
@layer reset, base, tema;
@layer base {
#painel-matricula .nota { --vencedor: "#id .nota, na layer base"; }
}
@layer tema {
.nota { --vencedor: ".nota, na layer tema"; }
}Uma classe simples derrubou um id, sem !important e sem gambiarra, só porque
a camada dela foi declarada depois. É este o valor prático do @layer: a
primeira linha do seu CSS declara a ordem de prioridade do projeto inteiro, e
o resto do arquivo para de brigar.
Um uso clássico é embrulhar o CSS de terceiro. @layer terceiros { ... } em
volta do arquivo de uma biblioteca coloca o seletor mais pesado dela abaixo do
seu código, e você volta a estilizar com classes normais.
O que a camada não alcança é o degrau 2 da escada. Colocando o mesmo
<section> com style da seção anterior contra um id na camada mais alta
declarada, o style do HTML ainda ganha:
@layer base, tema;
@layer tema {
#painel-matricula { --vencedor: "#id na layer tema"; }
}Como está a especificidade do seu CSS hoje
Dá para medir. O mesmo pacote da calculadora, ligado ao PostCSS, percorre uma folha inteira e conta quantos seletores existem em cada faixa:
const processor = parser();
const raiz = postcss.parse(readFileSync(process.argv[2], 'utf8'));
const contagem = new Map();
let importantes = 0;
raiz.walkRules((regra) => {
if (regra.parent?.name === 'keyframes') return;
for (const seletor of regra.selectors) {
const { a, b, c } = selectorSpecificity(processor.astSync(seletor).nodes[0]);
const chave = `${a},${b},${c}`;
contagem.set(chave, (contagem.get(chave) ?? 0) + 1);
}
regra.walkDecls((d) => {
if (d.important) importantes++;
});
});Rodei em cima do prose.css deste blog — o arquivo que formata o texto que
você está lendo agora:
node auditar.mjs src/styles/prose.cssSetenta e seis seletores, nenhum com id na primeira coluna e nenhum
!important. O pico está em 0,1,1 — uma classe mais uma tag, o formato de
.prose h2, que é literalmente a primeira regra dessa faixa no arquivo. O
seletor mais pesado do arquivo inteiro é o único 0,5,0:
:root[data-theme="light"] .prose .callout-atencao .callout-titulo, cinco
peças na coluna do meio porque :root e [data-theme="light"] também pontuam
ali, junto com as três classes.
Esse formato é o que você quer perseguir: uma folha achatada, em que quase tudo
mora entre 0,1,0 e 0,2,0. Quando a distribuição fica achatada, qualquer
regra nova sobrescreve qualquer regra antiga com uma classe a mais — e a
pergunta “por que isso não está pegando?” simplesmente para de aparecer.
Três hábitos levam até lá:
- Id fica para o JavaScript e para a âncora do link. No CSS, use classe. Um id no seletor cria um teto que nenhuma classe alcança depois.
- Selecione o elemento, não o caminho até ele.
.notadiz o que a coisa é;#painel-matricula .card-aluno span.notadescreve onde ela estava no dia em que você escreveu, e quebra na primeira mudança de HTML. - Declare as camadas antes de precisar delas. Uma linha
@layer reset, base, componentes, utilitarios;no topo do projeto resolve antecipadamente uma discussão que, sem ela, vira!important.
E vale separar dois conceitos que costumam ser confundidos: especificidade resolve conflito entre regras que casam com o mesmo elemento. Quando a propriedade nem foi declarada para aquele elemento, quem responde é a herança do CSS, que é outro mecanismo.
O que vem depois
O próximo degrau da trilha de CSS é o box model — sair de “qual regra vence” e entrar em “quanto espaço cada caixa ocupa”. Para observar essas disputas dentro de componentes, continue por CSS no React.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
Especificidade é a mesma coisa que ordem no arquivo?
Usar id no CSS é errado?
Dá para diminuir a especificidade de um seletor que eu não controlo?
O !important do navegador do usuário ganha do meu?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0 e Chrome 151.0.7922.34, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — Specificity — developer.mozilla.org
- CSS Cascading and Inheritance Level 5 — Cascade Sorting Order — w3.org
- Selectors Level 4 — Calculating a selector's specificity — w3.org



