VocĂȘ escreve .sidebar .card { color: blue; }, carrega a pĂĄgina e o card continua vermelho por causa de uma regra adicionada uma hora antes: .card { color: red; }. A nova regra vem depois no arquivo. Mesmo assim, perde. O navegador compara os dois seletores com um sistema de pontos fixo, e .sidebar .card pontua mais alto que .card, nĂŁo importa qual vocĂȘ escreveu por Ășltimo.
Esse sistema de pontos Ă© a especificidade. Depois que vocĂȘ aprende a calculĂĄ-la de cabeça, “por que meu CSS nĂŁo se aplica” se torna aritmĂ©tica pura. NĂŁo tem nada a ver com layout: Grid ou Flexbox decide onde um elemento fica, a especificidade decide qual regra define a cor ou o tamanho quando duas regras entram em conflito.
Como o navegador pontua um seletor
Cada seletor tem um peso formado por trĂȘs nĂșmeros, geralmente escritos como um trio separado por hĂfens: ID-classe-tipo. Some o que o seletor contĂ©m, coluna por coluna:
| Coluna | O que conta | Peso por ocorrĂȘncia |
|---|---|---|
| ID | #header | 1-0-0 |
| Classe / atributo / pseudoclasse | .card, [type="radio"], :hover, :nth-child(2) | 0-1-0 |
| Tipo / pseudoelemento | div, h1, ::before | 0-0-1 |
O seletor universal * e os combinadores (o espaço, >, ~, +) somam 0-0-0. Eles decidem quais elementos correspondem, não com que força a regra vence.
Alguns seletores reais:
| |
Para comparar dois valores, vå da esquerda para a direita; a primeira coluna que divergir decide. 1-1-0 vence 0-2-0 porque a coluna do ID (1 contra 0) decide antes de alguém olhar para as classes. Assim, #main .card vence .sidebar .card .title .text, não importa quão longo seja o segundo seletor. Um ID supera qualquer quantidade de classes.
Por que uma regra CSS mais recente pode continuar perdendo
Voltando ao exemplo inicial, com os nĂșmeros calculados:
| |
0-2-0 vence 0-1-0 na coluna das classes, entĂŁo o card fica azul independentemente da ordem no arquivo. A ordem no arquivo Ă© o critĂ©rio ao qual quase todo mundo recorre primeiro, mas sĂł vale quando os trios sĂŁo idĂȘnticos:
| |
As duas regras pontuam 0-1-0, entĂŁo vence a que aparece mais tarde na folha de estilo (ou no bloco <style> posterior, ou na folha vinculada depois da outra). Esse Ă© o Ășnico caso em que “a Ășltima vence” Ă© o modelo mental correto. Funciona em projetos pequenos, por isso Ă© a primeira coisa que todo mundo aprende, e para de funcionar assim que um projeto passa de um punhado de seletores.
Os estilos inline ficam acima de tudo isso. Um atributo style="color: green" no elemento vence qualquer regra em qualquer folha de estilo, não importa quantos IDs ela encadeie. Só as declaraçÔes !important conseguem superå-lo.
| |
| |
!important nĂŁo faz parte do trio de especificidade. Ă um nĂvel separado, de prioridade mais alta: qualquer declaração !important vence qualquer declaração normal, estilos inline incluĂdos, independentemente do peso do seletor. Quando duas declaraçÔes !important entram em conflito, a especificidade decide (depois a ordem no arquivo), exatamente como sem a flag. Ă assim que um Ășnico !important se transforma numa corrida armamentista. O prĂłximo desenvolvedor que precisar sobrescrevĂȘ-lo nĂŁo tem outra jogada alĂ©m de outro !important, e logo ninguĂ©m sabe mais qual regra deveria vencer.
Quais seletores somam zero especificidade: :where() contra :is()
Alguns seletores permitem direcionar com precisão sem pagar o preço normal.
:where() sempre contribui com 0-0-0, independentemente do que vocĂȘ colocar dentro:
| |
Isso torna :where() o invólucro certo para valores padrão e estilos de tema que precisam ser fåceis de sobrescrever. Uma biblioteca de componentes pode entregar seus padrÔes dentro de :where(), e uma regra simples .card { } de quem a usa vence sem precisar de !important.
:is(), :not() e :has() funcionam de forma diferente. NĂŁo somam peso prĂłprio, mas assumem o peso do seu argumento mais especĂfico:
| |
EntĂŁo :where() Ă© gratuito, e :is() custa tanto quanto o mais pesado que vocĂȘ colocar dentro, mesmo que esse argumento nunca corresponda ao elemento.
Como as cascade layers sobrepÔem a especificidade
Se vocĂȘ estĂĄ acumulando classes e IDs para ganhar disputas que nĂŁo tĂȘm nada a ver com o quĂŁo especĂfico um seletor deveria ser, o projeto precisa de @layer, nĂŁo de mais aninhamento.
| |
A ordem das layers vem de onde vocĂȘ declara os nomes, nĂŁo do peso dos seletores. utilities foi declarada por Ășltimo, entĂŁo uma regra dentro dela vence uma regra em components, mesmo que .text-blue tenha o mesmo peso 0-1-0 que .card. A especificidade continua resolvendo conflitos dentro de uma layer. Entre layers, a ordem das layers vence sem apelação e a especificidade nunca Ă© consultada.
Uma regra pega quase todo mundo de surpresa na primeira vez: para declaraçÔes normais (sem !important), estilos fora de qualquer layer vencem estilos dentro de uma layer. VocĂȘ move seu CSS de componentes para @layer components mas deixa uma sobrescrita sem layer, e essa regra sem layer vence independentemente da ordem em que vocĂȘ declarou as layers. Para declaraçÔes !important, a precedĂȘncia se inverte: as regras important da layer declarada primeiro vencem as das layers posteriores, e os estilos important sem layer ficam no Ășltimo lugar de todos. A inversĂŁo Ă© intencional, e Ă© a parte da especificação que vale reler antes de depender dela.
O suporte dos navegadores jĂĄ deixou de ser um argumento contra as layers hĂĄ um tempo: @layer roda em todos os engines principais desde 2022. O custo real Ă© organizacional. O time precisa combinar uma ordem de layers antecipadamente (reset, base, components, utilities Ă© um esquema comum) e encaixar o CSS novo na layer certa. Ă mais um problema de processo do que de sintaxe.
Erros comuns com especificidade CSS
- Combater especificidade com mais especificidade. Cada sobrescrita adiciona uma classe ou um ID mais pesado que a anterior, até mudar uma cor exigir
#app .page .section .card.is-active.is-selected. à o mesmo padrão de falha do spaghetti code: cada correção adiciona mais confusão em vez de remover. A solução quase nunca é um seletor mais pesado. à um mais plano, ou uma layer. - Recorrer a
!importantno primeiro conflito. Ganha a disputa imediata e prepara a prĂłxima, porque a prĂłxima sobrescrita nĂŁo tem mais nenhuma jogada de especificidade normal disponĂvel. - Esquecer que pseudoclasses contam.
:hover,:focus,:nth-child()e o argumento de:not()somam todos0-1-0. Um simplesa { color: blue; }mais abaixo no arquivo nĂŁo vai sobrescrevera:not(.disabled) { color: red; }escrito acima, porque a versĂŁo com:not()Ă© mais pesada. - Assumir que o valor herdado vence porque a regra “parece” mais forte. Uma regra que aponta diretamente para um elemento sempre vence um valor herdado, qualquer que seja a especificidade da regra de onde vem a herança. Se um filho mostra a cor errada, procure uma regra que aponte para esse filho antes de suspeitar da herança.
- Misturar
:is()num seletor esperando que continue barato.:is()assume o peso do seu argumento mais pesado, então:is(.card, #sidebar)é na pråtica um seletor1-0-0disfarçado.
Como verificar a especificidade no DevTools
VocĂȘ nĂŁo precisa contar colunas na mĂŁo quando um bug estĂĄ na sua frente. Inspecione o elemento no Chrome ou Firefox DevTools e abra o painel Styles (Rules no Firefox). Cada regra correspondente aparece listada em ordem de cascata, com as declaraçÔes sobrescritas tachadas. Passe o mouse sobre um seletor e os dois navegadores mostram sua especificidade. A partir daĂ vocĂȘ sabe em segundos se estĂĄ diante de uma diferença de especificidade, um problema de ordem de layers, ou simples ordem de arquivo entre duas regras de peso igual.
Na próxima vez que uma regra perder sem motivo aparente, calcule os dois trios antes de adicionar uma classe ou um !important para forçå-la. Normalmente, a correção certa é tirar peso do seletor que estå vencendo, não empilhar mais peso no que deveria ter vencido.