CSS Selectors, the Cascade and Specificity Explained
Web DevelopmentLesson 1 of 45 min read
What you'll learn
- Write type, class, id, attribute, combinator and pseudo-class selectors
- Calculate specificity as an (ids, classes, elements) triple and predict which rule wins
- Explain the order the cascade uses: importance, specificity, then source order
- Use inheritance deliberately and debug overridden declarations in DevTools
On this page
A plant nursery prints a price tag for each plant. The designer wants every price in green. She writes color: green, reloads, and the price is purple. She did not write purple anywhere near that rule. Welcome to the most common CSS confusion: several rules match one element and the browser has to choose. The mechanism is called the cascade, and once you can compute it by hand you will stop guessing and start fixing.
Selectors choose the elements
A CSS rule is a selector plus a block of declarations. The selector decides which elements are affected. Start with the building blocks, then combine them.
| Selector | Matches | Example |
|---|---|---|
| Type | Every element of that tag | p |
| Class | Elements whose class list contains the name | .price |
| Id | The element with that id | #fern |
| Attribute | Elements with a matching attribute | a[href^="https"] |
| Descendant | Inside an ancestor, at any depth | .tag .price |
| Child | Direct children only | .tag > h2 |
| Adjacent sibling | The element right after another | h2 + p |
| Pseudo-class | An element in a certain state or position | li:nth-child(odd), a:hover, input:focus-visible |
| Pseudo-element | A generated part of an element | .price::before |
Whitespace is meaningful. .tag.sale (no space) matches one element that has both classes. .tag .sale (with a space) matches a .sale element somewhere inside a .tag. That single space is behind a large share of "my selector does nothing" questions. Recent browsers also support :is(), :where() and :has() for grouping and for selecting a parent based on its children; check compatibility tables before using :has() in production.
A page that fights back
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Price tag</title>
<style>
#fern .price { color: purple; }
p { color: grey; }
.tag .price { color: green; }
article p.price{ color: red; }
.tag { font-family: Georgia, serif; border: 1px solid #2d4a2d; padding: 12px; width: 220px; }
</style>
</head>
<body>
<article class="tag sale" id="fern">
<h2 class="name">Boston Fern</h2>
<p class="price">12.50</p>
<p class="note">Water twice a week</p>
</article>
</body>
</html>The page shows a 220-pixel-wide card with a thin dark-green border and 12 pixels of padding. Inside: "Boston Fern" as a bold heading, the price in purple, and the note in grey. All four colour rules match the price paragraph, and purple is declared first, so how did it win? Count the parts of each selector.
| Selector | ids | classes, attributes, pseudo-classes | elements | Specificity |
|---|---|---|---|---|
p | 0 | 0 | 1 | (0, 0, 1) |
.tag .price | 0 | 2 | 0 | (0, 2, 0) |
article p.price | 0 | 1 | 2 | (0, 1, 2) |
#fern .price | 1 | 1 | 0 | (1, 1, 0) |
Compare the columns from left to right, never adding them together. One id outranks any number of classes, and one class outranks any number of element names. #fern .price has (1, 1, 0), which beats (0, 2, 0) at the first column, so purple wins even though it is the first of the four rules in the file and three later rules also match. Source order only breaks ties between equal specificity.
The order the cascade uses
- Importance and origin. A declaration with
!importantbeats a normal one, and your styles generally beat the browser's default styles. - Cascade layers. If you use
@layer(supported in current browsers), a later layer beats an earlier one for normal declarations, regardless of specificity. - Specificity. Inline
style=""beats any selector; then ids, then classes, attributes and pseudo-classes, then types and pseudo-elements. - Source order. If everything above is a tie, the declaration that appears last wins.
Inheritance is a separate mechanism. Some properties, such as color, font-family and line-height, pass from parent to child when the child has no rule of its own. The nursery card sets font-family: Georgia on .tag, and the heading and both paragraphs pick it up without being targeted. But inheritance is the weakest influence there is: any rule that directly matches an element, even p { color: grey } with specificity (0, 0, 1), beats an inherited value. Properties like border, margin and padding do not inherit.
Fixing it without a fight
The wrong fix is !important: it raises the stakes, and the next person must also use it. The right fix is to remove the accidental high-specificity rule. Here the id selector is the problem, so style by class and let source order decide.
/* Base: low specificity, easy to override */
p { color: grey; margin: 0 0 8px; }
/* Components: one class each */
.tag { font-family: Georgia, serif; }
.price { color: green; font-weight: 700; }
.note { font-size: 0.875rem; }
/* State: slightly higher specificity on purpose */
.tag.sale .price { color: #b3261e; } /* (0, 3, 0) */
.price::before { content: "\0024 "; } /* adds a dollar sign visually */Now the sale tag shows the price in red, bold, with a generated dollar sign before the number, while a non-sale tag would show it green. The card's colours are decided by one class each, plus one deliberate extra-specific rule, so it is obvious which rule is supposed to win. This is the same idea behind naming systems such as BEM: keep selectors flat so specificity stays predictable.
Common mistakes
- Forgetting the dot or hash.
price { color: green }targets a<price>element that does not exist, so nothing happens and nothing errors. - A typo in a property or value.
colour: greenorcolor: grenis dropped silently; DevTools shows the declaration with a warning icon and strikethrough. - Missing semicolon.
color: greenfollowed byfont-weight: 700on the next line merges into one invalid declaration, so both are lost. - Reaching for `#id` selectors in a stylesheet. One id rule can only be beaten by another id or
!important, and your later class rules mysteriously fail. - `!important` everywhere. Overrides now need
!importanttoo, and the stylesheet turns into an arms race. - A stale stylesheet. You edit the file and see no change because the browser cached it; hard-reload or tick "Disable cache" in DevTools.
How this shows up in real projects and interviews
Specificity wars are a staple of legacy code, and "how does CSS decide which rule applies?" is a classic front-end interview question. The answer to give is the ordered list above, followed by a worked specificity comparison like the table. In a component framework, scoped styles or utility classes reduce conflicts, but they still sit on top of the same cascade. If you work in React, see React Components and Props for how class names flow into the markup that your CSS targets.
Try it yourself
Create an HTML page with a navigation list of five links, one of which has class current. Style it so links are blue, the current link is bold and dark red, every second list item has a light grey background, and external links (href starting with https) get a small arrow after them. Write down the specificity of each of your selectors, and make one rule deliberately lose, then explain why using DevTools.
Show a hint
Use a[href^="https"]::after, li:nth-child(even) and a.current. To make one lose, put a more specific selector later or earlier and see which Styles entry is struck through.
Key takeaways
- Selectors pick elements; whitespace and combinators change meaning (
.a.bversus.a .b). - Specificity is a triple compared column by column: ids, then classes and attributes, then element names.
- The cascade order is importance, layers, specificity, then source order; inheritance is weaker than any direct rule.
- Keep selectors flat and class-based, avoid
!important, and read the struck-through rules in DevTools.
Test yourself
A trick from this lesson's ride: A space means 'inside of'; no space means 'also is'. Four more tricks like it wait along the route, each followed by a question.
Frequently asked questions
How is CSS specificity calculated?
Count the id selectors, then the class, attribute and pseudo-class selectors, then the element and pseudo-element selectors, and compare the three numbers from left to right. #nav .item is (1, 1, 0) and beats .nav .item .link at (0, 3, 0). Inline styles beat all of them, and !important beats normal declarations.
Why is my CSS not applying to the element?
Inspect the element and check the Styles pane. Usual causes are a selector typo, a more specific rule overriding yours, a property misspelled or given an invalid value, a stylesheet that did not load, or a cached old file. A struck-through declaration means it lost the cascade.
What is the difference between a class and an id in CSS?
A class can be reused on many elements and one element can have several, while an id must be unique within a page. Ids also have much higher specificity, which makes them hard to override. For styling, prefer classes and keep ids for fragment links and form labels.