CSS Selectors and Specificity: Elements, Classes, IDs, Attributes, and Combinators
Specificity is the three-column score browsers use to settle conflicts between CSS rules: ID, class, type. Learn what each selector is worth, why combinators and the universal selector add nothing, and how to compare two competing rules column by column — then check yourself with six practice questions.
A CSS selector is the pattern that tells the browser which elements a rule applies to, and specificity is the scoring system the browser uses to pick a winner when two rules target the same element and set the same property. Specificity is counted in three columns — ID, class, type — usually written like 1-2-1, and the browser compares those columns from left to right. Because the columns never carry over the way ordinary digits do, one ID selector beats any number of class selectors, and one class selector beats any number of type selectors.
The selectors and what each one is worth
- Type (element) selector —
p,h1,ul. Matches every element with that tag name. Weight0-0-1. - Class selector —
.card. Matches any element whoseclassattribute includes that word. Weight0-1-0. - ID selector —
#nav. Matches the element carrying thatid. Weight1-0-0. - Attribute selector —
[type="email"]. Worth the same as a class,0-1-0. Operators let you match part of a value:^=the value starts with,$=the value ends with,*=the value contains the substring anywhere,~=the value is a space-separated list containing that exact whole word,|=the value is exactly that or begins with it immediately followed by a hyphen. - Pseudo-class — one colon:
:hover,:first-child,:checked. Describes a state or a position rather than markup. Weight0-1-0. - Pseudo-element — two colons:
::before,::first-line. Styles a piece of an element that has no tag of its own. Weight0-0-1, the same as a type selector. - Universal selector —
*. Matches everything and adds nothing:0-0-0.
Combinators describe relationships
A B(a space) — descendant: anyBanywhere inside anA, at any depth.A > B— child: only aBthat is a direct child ofA.A + B— next sibling: the singleBthat immediately followsAunder the same parent.A ~ B— subsequent sibling: everyBthat followsAunder the same parent, immediately or not.
Combinators change what gets matched, but they add nothing to the score. #main a and #main > a both weigh 1-0-1.
Adding it up
Break the selector into its simple parts and drop each one into its column. #nav ul li a.active has one ID, one class, and three type selectors, so it scores 1-1-3. Then compare the competing rules: ID column first; if that ties, the class column; if that ties too, the type column. Only when all three columns tie does position matter, and then — among declarations of the same origin and cascade layer — the one written later wins.
Two things sit outside the selector score entirely. A declaration in an inline style attribute outranks any normal declaration from a stylesheet, no matter how specific that selector is. And a declaration flagged !important in an author stylesheet overrides normal declarations from the same origin, including a normal inline one. Both are worth avoiding while you're learning, because they paper over the cascade instead of working with it.
For the wider HTML and CSS picture, see the WGU D276 Web Development Foundations study guide.
Practice: Css Selectors Specificity
-
What is the specificity of the selector #nav ul li a.active ?
- 1-1-2
- 1-1-3
- 0-1-4
- 0-0-5
Show answer & explanation
Correct answer: 1-1-3. Sort the parts into three columns: #nav is one ID, .active is one class, and ul, li, and a are three type selectors, giving 1-1-3. The tempting wrong answer is 0-0-5, which comes from counting five simple selectors as one flat total; specificity is never a single number, and an ID can never be traded for type selectors no matter how many you stack up.
-
A page contains an h2 element followed by two sibling paragraphs, all three sharing the same parent. You want to style only the paragraph that comes immediately after the heading. Which selector does that?
- h2 ~ p
- h2 > p
- h2 + p
- h2 p
Show answer & explanation
Correct answer: h2 + p. The next-sibling combinator (+) matches an element only if it is immediately preceded by its sibling under the same parent, so h2 + p hits just the first paragraph. The close call is h2 ~ p: the subsequent-sibling combinator matches every following sibling paragraph, so it would style both. The other two match nothing here, since > and the descendant space both require the paragraph to be nested inside the h2, not beside it.
-
A stylesheet contains #page article p { color: navy; } and, later in the same file, .wrapper .content .intro p.lead { color: crimson; }. Both rules match the same paragraph. What color is the text, and why?
- Crimson, because it uses more simple selectors, so its total weight is higher
- Navy, because a single ID outweighs any number of classes — the ID column is compared first
- Crimson, because when two rules match, the one written later in the stylesheet always wins
- Navy, but only because the navy rule appears first in the stylesheet
Show answer & explanation
Correct answer: Navy, because a single ID outweighs any number of classes — the ID column is compared first. The first rule scores 1-0-2 and the second scores 0-4-1. The browser compares the ID column first, and 1 beats 0, so navy applies and the other columns are never consulted. The trap is assuming totals add up — four classes plus a type looks bigger than one ID plus two types, but columns don't carry, and source order only breaks a tie when all three columns are equal.
-
What is the specificity of * .sidebar ul li a:hover ?
- 0-2-4
- 0-3-3
- 0-2-7
- 0-2-3
Show answer & explanation
Correct answer: 0-2-3. The class column gets .sidebar and the pseudo-class :hover for 2, and the type column gets ul, li, and a for 3, giving 0-2-3. The universal selector * contributes 0-0-0 and combinators add nothing at all, so the distractors each come from crediting something weightless: 0-2-4 counts the asterisk as a type selector, 0-3-3 counts it as a class, and 0-2-7 adds the four descendant combinators.
-
Which selector matches only anchor elements whose href value ends with ".pdf"?
- a[href*=".pdf"]
- a[href$=".pdf"]
- a[href^=".pdf"]
- a[href~=".pdf"]
Show answer & explanation
Correct answer: a[href$=".pdf"]. The $= operator is the suffix match, so a[href$=".pdf"] matches only values ending in ".pdf". The near-miss is *=, which matches the substring anywhere — it would also catch href="/report.pdf.html" or "/pdf-guide/". The ^= operator matches a prefix, so it would need an href literally starting with ".pdf", and ~= splits the value on whitespace and requires one whole word to equal ".pdf" exactly, which "/guide.pdf" is not.
-
Which statement about pseudo-classes and pseudo-elements is correct?
- Both are written with two colons and both count in the class column
- Neither one affects specificity, so p:hover and p carry the same weight
- :hover is a pseudo-class written with one colon and counts in the class column, while ::before is a pseudo-element written with two colons and counts in the type column
- ::before counts in the class column because, like :hover, it begins with a colon
Show answer & explanation
Correct answer: :hover is a pseudo-class written with one colon and counts in the class column, while ::before is a pseudo-element written with two colons and counts in the type column. Pseudo-classes such as :hover weigh 0-1-0, the same as a class or attribute selector, while pseudo-elements such as ::before weigh 0-0-1, the same as a type selector. The tempting error is lumping them together because both start with a colon — that would make p::before score 0-1-1 instead of its actual 0-0-2, and it would predict the wrong winner against a rule like .intro p.
A worked example
Here is one link that four different rules all try to color:
<nav id="main">
<ul class="menu">
<li><a class="link active" href="/pricing">Pricing</a></li>
</ul>
</nav>
| Rule | ID | Class | Type |
|---|---|---|---|
a | 0 | 0 | 1 |
.menu a | 0 | 1 | 1 |
.menu .link.active | 0 | 3 | 0 |
#main a | 1 | 0 | 1 |
#main a wins, and it isn't close: three chained class selectors lose before the class column is ever read, because the ID column is compared first and 1 beats 0. Notice also that .link.active counts as two classes even though it targets one element — specificity is read off the selector as it's written, so a repeated simple selector counts every time it appears, not once per element matched.
Mistakes people actually make
- Adding the columns into a single total.
0-4-1is not "five." Compare left to right and stop at the first column that differs. - Assuming the last rule always wins. Source order is the tiebreaker, not the first test. It only applies when all three columns match exactly.
- Expecting combinators to add weight. Rewriting
.menu aas.menu > li > aadds two type selectors, not "child selector power." - Treating
[id="main"]like#main. They match the same element, but the attribute form weighs0-1-0and the ID form weighs1-0-0. - Reaching for
!important. It wins, then the next conflict needs another one. Adding a class to the element you actually want to style is almost always the better fix.
Two syntax details worth knowing
For backward compatibility, browsers still accept single-colon spellings of the four original pseudo-elements — :before, :after, :first-line, and :first-letter. The colon count is only a spelling convention there; those are pseudo-elements either way, so they still land in the type column. The double-colon form is the one to write in new code, and every pseudo-element added since then requires it.
A few pseudo-classes score differently from the flat 0-1-0 above. :where() always contributes 0-0-0, whatever you put inside it, which makes it useful for writing defaults that are easy to override. :is(), :not(), and :has() contribute nothing themselves but take on the weight of their most specific argument, so p:not(#fakeId) weighs 1-0-1 even though no element with that ID has to exist.
How to reason about the tricky cases
Start by confirming the two rules really do collide: they must match the same element and set the same property. A common false alarm is inheritance — a color inherited from body loses to any rule that matches the child directly, however weak that rule is, because an inherited value is only used where no declaration targets the element itself.
When you're stuck, write both selectors out and tally three columns on paper before you touch the code. If a rule still refuses to apply, check for an inline style attribute in the HTML, which outranks every normal stylesheet declaration. And when you catch yourself escalating — adding an ID just to win — that's the signal to lower the other rule instead. Keeping most of your selectors at a single class keeps the whole sheet predictable.
Work through more of these in the D276 practice question set, and keep the MDN specificity reference open while you experiment.
Want someone to walk you through this?
Book 1-on-1 prep for WGU D276 — original coaching, never exam content.
Stuck on this topic? WhatsApp us on +1 646 980 4914.
Working through the whole course? Read the full WGU D276 study guide, or see every WGU D276 practice topic.