Selectors & cascade.
How CSS decides what to style.
Before you can style anything, you need to target it. That's what selectors do. And before you can predict what a page will look like, you need to understand why one rule wins over another. That's the cascade and specificity. This lesson covers both — the foundation everything else in CSS sits on. Every example has a Preview button: click to see the rendered result inline.
Four selector types cover 90% of styling: element (p, h1), class (.button), id (#header), and attribute ([type="text"]). Use classes for anything reusable. When rules conflict, specificity decides the winner: inline (1000) > id (100) > class (10) > element (1). The cascade then applies later rules over earlier ones when specificity ties. Understanding both is the difference between "CSS works" and "CSS works predictably."
Before this lesson: you need basic HTML. If you haven't finished the HTML track, do that first — you can't select elements you can't write.
🎯 The four selector types
Each one matches elements differently. Each has a different specificity score.
/* Element selector — matches every p tag */
p {
color: #64748b;
line-height: 1.6;
}
/* Class selector — matches every element with class="highlight" */
.highlight {
background: #fef3c7;
padding: 4px 8px;
border-radius: 4px;
}
/* ID selector — matches the one element with id="main-title" */
#main-title {
color: #1572B6;
font-size: 1.75rem;
margin-bottom: 1rem;
}
/* Compound selector — button elements with class="primary" */
button.primary {
background: #1572B6;
color: white;
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
}
CSS Selectors Demo
This paragraph uses an element selector — every p tag on the page gets this styling. The highlighted text uses a class selector, which can be reused many times.
Element selector
p, h1, div. Matches every element of that type. Specificity: 1 point. Use for base styles — font size, color, line-height for all paragraphs.
Class selector
.button. Matches every element with that class. Specificity: 10 points. Use for anything reusable — buttons, cards, alerts. This is what you'll write 80% of the time.
ID selector
#header. Matches the one element with that id. Specificity: 100 points. Use for page-specific anchors and JavaScript targets. Avoid for styling — too hard to override.
Compound selector
button.primary. Matches button elements with class="primary". Specificity: 11 points (1 + 10). Use when you want a class scoped to one element type.
Default to classes. Element selectors are too broad (you'll style things you didn't mean to). ID selectors are too specific (you'll fight to override them later). Classes are the sweet spot: reusable, predictable, and easy to override when needed. This is why every CSS framework, design system, and modern codebase uses classes almost exclusively.
🔗 Combinators — match based on relationship
Four ways to combine selectors. Each means something different.
/* Descendant (space) — any p inside .card, at any depth */
.card p {
color: #64748b;
margin: 0 0 8px 0;
}
/* Child (>) — only p that is a DIRECT child of .card */
.card > p {
font-weight: 600;
}
/* Adjacent sibling (+) — the first p immediately after an h3 */
h3 + p {
color: #1572B6;
}
/* General sibling (~) — every p after an h3, at the same level */
h3 ~ p {
line-height: 1.6;
}
/* .card style (for preview) */
.card {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
}
Card title
This is a direct child of .card — gets the strong weight and the blue color from h3+p.
This is also a direct child — the descendant selector applies, but not h3+p (not immediately after h3).
This p is inside a nested div — the descendant selector still matches it, but .card > p does not (it's not a direct child).
Space (descendant)
.card p — any p inside .card, at any depth. Most flexible, but too broad when you have nested structures.
> (child)
.card > p — only p that is a direct child of .card. Skips nested levels. More precise than descendant.
+ (adjacent sibling)
h3 + p — the first p that immediately follows an h3 as a sibling. Use for "first paragraph after a heading" styling.
~ (general sibling)
h3 ~ p — every p that follows an h3, at the same level. Broader than adjacent sibling. Use for "all subsequent paragraphs."
Descendant combinators are surprisingly expensive. .card p requires the browser to check every p on the page against every .card ancestor. For most pages, this is fast enough. But on large pages with deep nesting, prefer the child combinator (.card > p) — it stops searching once it hits a non-direct child, which is much cheaper. Modern browsers optimize heavily, but it's a good habit.
✨ Pseudo-classes — state and position
Select elements based on what's happening to them, not just what they are.
/* :hover — when mouse is over the element */
button:hover {
background: #33A9DC;
transform: translateY(-1px);
}
/* :focus — when element has keyboard focus */
button:focus {
outline: 2px solid #1572B6;
outline-offset: 2px;
}
/* :first-child — the first child of its parent */
li:first-child {
font-weight: 700;
color: #1572B6;
}
/* :last-child — the last child of its parent */
li:last-child {
color: #64748b;
}
/* :nth-child(n) — the nth child (1-indexed) */
li:nth-child(2) {
background: #f0f9ff;
padding-left: 8px;
}
/* :nth-child(odd/even) — alternating rows */
tr:nth-child(odd) {
background: #f8fafc;
}
/* Base styles for preview */
button {
background: #1572B6;
color: white;
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
ul { list-style: none; padding: 0; margin: 0; }
li { padding: 6px 0; }
Hover the button to see :hover. Tab to it to see :focus.
Pseudo-classes work on lists too:
:hover
When the mouse is over the element. Used for buttons, links, cards — every hover effect. Doesn't work on touch devices (no hover state).
:focus
When the element has keyboard focus. Required for accessibility. Buttons, links, and form fields all get keyboard focus when tabbed to.
:first-child / :last-child
Matches the first (or last) child of its parent. Use to remove the top margin on the first item or bottom margin on the last — classic spacing fix.
:nth-child(n)
Matches the nth child (1-indexed). Also accepts odd, even, and formulas like 2n+1. The classic way to style alternating table rows.
:focus-visible is better than :focus. :focus applies whether the user clicked or tabbed. :focus-visible applies only when the browser thinks focus should be visible — usually keyboard focus, not mouse click. Use :focus-visible for focus rings so mouse users don't see an outline after clicking a button. It's supported in every modern browser.
⚖️ Specificity — who wins
The four-number score that decides which rule applies. Learn this once, debug CSS forever.
When two CSS rules both target the same element, the browser needs a way to pick one. That's specificity: a score calculated from the selector's parts. Higher score wins. If scores tie, the later rule wins. Understanding this is what turns "CSS works sometimes" into "CSS works predictably."
/* Score: (inline, id, class, element) */ p → (0, 0, 0, 1) = 1 .text → (0, 0, 1, 0) = 10 p.text → (0, 0, 1, 1) = 11 #header → (0, 1, 0, 0) = 100 #header .text → (0, 1, 1, 0) = 110 style="color: red" → (1, 0, 0, 0) = 1000 !important → wins everything
/* Rule 1: element selector — score 1 */
p {
color: #64748b;
}
/* Rule 2: class selector — score 10 */
.note {
color: #1572B6;
}
/* Rule 3: compound — score 11 */
p.note {
color: #dc2626;
}
/* Rule 4: id selector — score 100 */
#special {
color: #8b5cf6;
}
/* Result: the paragraph below has id="special" + class="note"
• is a p element. All four rules match it.
Highest specificity wins: #special (100) beats p.note (11),
.note (10), and p (1). So the text will be purple. */
This is a plain paragraph — element selector wins. Gray.
This has class="note" — class selector beats element. Blue.
This has class="note" but p.note has higher specificity. Red.
This has id="special" + class="note". ID wins. Purple.
| Selector | Score | Use when |
|---|---|---|
p, div, h1 | 1 | Base styles for all elements of a type |
.button, .card | 10 | Reusable components (95% of your CSS) |
p.note, button.primary | 11 | Scope a class to one element type |
.card .title | 20 | Style children within a component |
#header | 100 | Page-specific anchors (rarely for styling) |
style="..." | 1000 | Never — inline styles are unmaintainable |
!important | ∞ | Only to override third-party CSS |
Never use !important to fix specificity. It overrides everything, which means every future override needs another !important. It turns a manageable cascade into a cascade war. If a rule isn't winning, use DevTools to see what's beating it, then fix the specificity — not with !important, but by making your selector more specific or reordering the CSS. The only valid uses of !important: overriding third-party CSS you can't edit, and utility classes like .hidden { display: none !important; }.
How to check specificity in DevTools. Right-click any element → Inspect. In the Styles panel, you'll see all rules that apply. Rules that are overridden are shown with a strikethrough. Click any rule to jump to its source file. This is how you debug "why isn't my CSS applying" — the answer is always in DevTools, no guessing required.
🌊 The cascade — when specificity ties
Same specificity? Later rule wins. That's the cascade.
/* Same specificity (class = 10), later rule wins */
.color-blue {
color: #1572B6;
}
.color-red {
color: #dc2626;
}
/* If an element has both classes, RED wins because
.color-red comes later in the CSS file. */
/* The order in the class attribute doesn't matter: /
/ class="color-blue color-red" and class="color-red color-blue" /
/ both resolve to red. */
/* Order of appearance in the HTML matters: /
/ then /
/ means theme.css rules win when specificity ties. */
Both classes below have specificity 10. The later rule (.color-red) wins.
Both classes applied — shows red
Source order matters
Later rules override earlier rules with the same specificity. This is why your CSS is applied in order — base styles first, then component styles, then page-specific.
Class order in HTML doesn't matter
class="a b" and class="b a" have the same effect. The browser looks at specificity and source order in the CSS file, not the HTML.
Stylesheet order matters
If you link two stylesheets, the later one wins for tied specificity. Load order: normalize/reset → base → components → utilities → page-specific.
Inheritance flows down
Children inherit font, color, and line-height from parents — unless they set their own. This is why you can set font-family on body and every element inherits it.
🏆 Challenge
Three tasks. Each exercises a different selector skill.
Task 1 — Predict the color
Given these rules (in order), what color will the text be for each element?
1. p { color: gray; }
2. .text { color: blue; }
3. p.text { color: red; }
4. #main { color: purple; }
Element A: → what color?
Element B: → what color?
Element C: (no class, no id) → what color?
Then verify by building the page and using DevTools to inspect each element. The Styles panel will show you exactly which rule is winning and why.
Task 2 — Style a list
Build an unordered list with 5 items. Style it so:
1. The first item is bold and blue
2. The last item is gray
3. The third item has a light background
4. Odd items (1, 3, 5) have a subtle border-bottom
Use :first-child, :last-child, :nth-child(3), and :nth-child(odd). Verify each rule applies by using the Preview button and inspecting in DevTools.
Task 3 — Fix the specificity problem
Here's a buggy CSS file. Why isn't .button winning?
.button { color: white; background: blue; }
a { color: purple; }
Click
The link is purple, not white. Fix it — without using !important.
Hint: .button has specificity 10, but a has specificity 1. The class should win. Something else is happening. Use DevTools to see what's actually overriding it — sometimes the answer is that a browser default (like a:link) is winning.
⚠️ Common mistakes
The selector and specificity bugs that catch every beginner.
#header { color: blue; } has specificity 100 — hard to override, breaks theming. Fix: use classes for styling, IDs only for anchors and JavaScript.
You add a class, it doesn't work. Why? Another rule has higher specificity. Fix: inspect in DevTools — the losing rule is crossed out. Then fix specificity, not with !important.
Every future override needs another !important. It cascades the problem instead of solving it. Fix: understand specificity, use more specific selectors, reorder CSS.
div div p matches too much and has specificity 3 (higher than you want). Fix: use classes — .card-content p. More specific intent, easier to override.
style="color: red" has specificity 1000 — overrides everything. Fix: use classes. Inline styles are unmaintainable and impossible to theme.
:hover is mouse, :focus is keyboard. Every interactive element should have both. Fix: style both — button:hover, button:focus { ... }. Or use :focus-visible for keyboard-only focus.
Two rules with the same specificity — the later one wins. If you add a rule at the top of the file expecting it to override, it won't. Fix: add rules after the rules they should override.
Setting color on every element instead of relying on inheritance from body. Fix: set global styles on body, override specific elements when needed. Less code, more predictable.
➡️ What's next
You can target elements. Next: understand the space they occupy.
❓ Frequently asked questions
What are CSS selectors?
CSS selectors are patterns that tell the browser which HTML elements to style. There are four main types: element selectors (p, h1), class selectors (.button), id selectors (#header), and attribute selectors ([type='text']). Combinators extend them based on relationships. Modern CSS also has pseudo-classes and pseudo-elements.
What is CSS specificity?
Specificity is a score that determines which rule wins when multiple rules target the same element. Inline styles = 1000. Each id = 100. Each class, attribute selector, or pseudo-class = 10. Each element = 1. Higher score wins. If tied, later rule wins. !important overrides everything but is a code smell.
What is the difference between class and id in CSS?
Class selectors can match many elements. ID selectors must be unique per page. IDs have higher specificity (100 vs 10), which makes them harder to override. In practice, use classes for styling, IDs only for anchors and JavaScript targets.
What is the difference between .class and element.class?
;.button matches any element with class 'button'. button.button matches only button elements with class 'button'. The compound selector has higher specificity (11 vs 10). Use compound when you want to scope a class to a specific element type.
What are combinators in CSS?
Combinators describe the relationship between selectors. Descendant (space): div p matches p inside div at any depth. Child (>): div > p matches only direct children. Adjacent sibling (+): h1 + p matches p immediately after h1. General sibling (~): h1 ~ p matches any p after h1 as a sibling.
What are pseudo-classes and pseudo-elements?
Pseudo-classes select elements based on state or position: :hover, :focus, :first-child, :nth-child(2). Pseudo-elements select a part of an element or insert content: ::before, ::after, ::first-line, ::selection. Pseudo-classes use one colon, pseudo-elements use two (though single colon works for the legacy four).
Why do my styles get overridden?
Three main causes: another rule has higher specificity, another rule comes later in the CSS with the same specificity, or another rule uses !important. Use DevTools to see which rule is winning — crossed-out styles are overridden. Fix by increasing specificity carefully, reordering CSS, or avoiding !important.
Should I use !important in CSS?
Almost never. !important overrides everything, which means every future override needs another !important. It turns a manageable cascade into a cascade war. The only valid uses: overriding third-party CSS you can't edit, and utility classes that must always win (like .hidden { display: none !important; }).