Overview

Specificity decides which rule wins within a cascade layer. Scores below are (IDs, classes/attributes/pseudo-classes, types/pseudo-elements); compare left to right. Layers and !important sit above specificity, see css-cascade-layers.

Basic and attribute selectors

Prefer classes over IDs; the specificity column shows why.

SelectorExampleSpecificity
Universal*(0,0,0)
Typep(0,0,1)
Class.btn(0,1,0)
ID#header(1,0,0)
Attribute present[disabled](0,1,0)
Attribute equals[type="email"](0,1,0)
Starts with / ends with[href^="https"], [src$=".svg"](0,1,0)
Contains[class*="card"](0,1,0)
Case-insensitive value[type="email" i](0,1,0)

Combinators

Use the child combinator when the structure is fixed.

CombinatorExampleMatches
Descendantnav aAny a inside nav.
Childul > liDirect children only.
Adjacent siblingh2 + pThe p right after an h2.
General siblingh2 ~ pEvery later p sibling of an h2.

Logical pseudo-classes

Use :where() for low-specificity defaults and :is() for grouped selectors.

SelectorExampleSpecificity
:is():is(h1, h2) + pHighest in the list.
:where():where(h1, h2) + pAlways (0,0,0); use for resets and base styles.
:not()li:not(.skip)Highest in the list.
:has()article:has(img)Highest in the list; selects the parent.

State and structural pseudo-classes (:hover, :nth-child()) score (0,1,0); see css-pseudo-classes.

Gotchas

Check these when a rule wins or loses unexpectedly.

GotchaFix
IDs and !important are hard to override.Style with classes and cascade layers.
:not(.a.b) matches elements missing either class.Use :not(.a):not(.b) for “neither”.
:is(.a, #b) scores like the ID.Put low-specificity alternatives in :where().
Unlayered rules beat layered rules whatever the specificity.Order @layer declarations deliberately.
Attribute values are case-sensitive, except for a few HTML attributes such as type.Add the i flag when unsure.
:has() reached all major browsers only in December 2023 (Firefox 121).Check your support target before relying on it.