Lesson 1

Selectors & cascade

Home › Courses › CSS › Lesson 1
🔵 Lesson 1 · 20 minutes

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.

~20 minutes Beginner CSS3 · modern
CSS path Selectors Combinators Pseudo-classes Specificity The cascade Challenge

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.

CSS
/* 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;
}

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.

CSS
/* 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;
}

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.

CSS
/* :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

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."

Specificity scores
/* 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
CSS — which wins?
/* 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. */
Selector Score Use when
p, div, h11Base styles for all elements of a type
.button, .card10Reusable components (95% of your CSS)
p.note, button.primary11Scope a class to one element type
.card .title20Style children within a component
#header100Page-specific anchors (rarely for styling)
style="..."1000Never — 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.

CSS
/* 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. */

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.

1. Using IDs for styling

#header { color: blue; } has specificity 100 — hard to override, breaks theming. Fix: use classes for styling, IDs only for anchors and JavaScript.

2. Forgetting specificity when a rule doesn't apply

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.

3. Using !important

Every future override needs another !important. It cascades the problem instead of solving it. Fix: understand specificity, use more specific selectors, reorder CSS.

4. Overly broad descendant selectors

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.

5. Inline styles

style="color: red" has specificity 1000 — overrides everything. Fix: use classes. Inline styles are unmaintainable and impossible to theme.

6. Confusing :hover and :focus

: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.

7. Forgetting source order

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.

8. Breaking inheritance

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; }).

🌐 External resources

← Back to CSS track
You're in the CSS track · Lesson 1