CSS Selectors & Specificity Grouping and Combining Selectors

In the previous lesson, we covered the four core selectors: universal, type, class, and ID. If you haven’t read that one yet, do that first. Today builds directly on top of it, and things get noticeably more powerful.

Here’s something that’s almost guaranteed to happen to you early on. You write something like this:

h1 {
  color: #ffffff;
  font-family: "Inter", sans-serif;
}

h2 {
  color: #ffffff;
  font-family: "Inter", sans-serif;
}

h3 {
  color: #ffffff;
  font-family: "Inter", sans-serif;
}

Same properties. Three separate blocks. Zero difference in output. And then you look at it and think: there has to be a better way.

There is. And that’s exactly what we cover in this article.

We’ll go through two core ideas: grouping selectors with commas (to stop repeating yourself), and CSS combinators (to target elements based on where they sit inside your HTML). By the end, you’ll write CSS that’s cleaner, shorter, and far more precise.


01Grouping CSS Selectors with the Comma

The comma in a CSS selector list simply means: “apply these styles to all of these.” That’s it.

h1, h2, h3 {
  color: #ffffff;
  font-family: "Inter", sans-serif;
}

Same result as three separate blocks. One block. Done.

This pattern is called a selector list, and you’ll see it absolutely everywhere in real stylesheets. CSS resets, for example, are almost entirely built using this. You’ll see something like:

*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

That’s three selectors sharing one rule block. Clean and compact.

You Can Mix Any Selector Types in a Group

Comma grouping isn’t limited to type selectors. You can throw classes, IDs, and type selectors all into the same list as long as the shared styles make sense for all of them:

h1, .page-title, #hero-heading {
  font-size: 2.5rem;
  font-weight: 700;
  line-height: 1.2;
}

.btn, .tag, .badge {
  border-radius: 6px;
  padding: 0.25rem 0.75rem;
  display: inline-block;
}

section, article, aside {
  box-sizing: border-box;
  width: 100%;
}
Worth Knowing

If any selector in your comma-separated group has a typo or syntax error, some browsers will ignore the entire rule block, not just that one selector. So double-check your comma-separated lists when styles aren’t applying.

Here’s a visual look at what grouping saves you:

Without Grouping
h1 { color: #ffffff; font-family: “Inter”; }
h2 { color: #ffffff; font-family: “Inter”; }
h3 { color: #ffffff; font-family: “Inter”; }

Output:

Heading One
Heading Two
Heading Three
12 lines for 3 elements
With Grouping
h1, h2, h3 { color: #ffffff; font-family: “Inter”; }

Output:

Heading One
Heading Two
Heading Three
4 lines for 3 elements

Exact same output. The grouping version is just three times shorter. Now imagine doing that across an entire stylesheet with 20+ similar rules.


02CSS Combinators: Selecting Elements by Their Relationship

This is where things get genuinely interesting. Until now, we’ve selected elements by what they are: their tag, their class, their ID. Combinators let you select elements by where they sit in the HTML structure, based on their relationship to other elements.

Think of HTML as a family tree. Elements have parents, children, and siblings. Combinators let you write CSS that says things like: “only target paragraphs that are inside this specific div” or “only target the element that immediately follows this heading.”

There are four CSS combinators:

  • The descendant combinator (a space character)
  • The child combinator (>)
  • The adjacent sibling combinator (+)
  • The general sibling combinator (~)

Let’s go through each one, because they all behave differently and that difference matters.


03The CSS Descendant Combinator: “Anywhere Inside”

This is the most commonly used combinator. It’s written as a space between two selectors:

/* Every <p> anywhere inside .article */
.article p {
  line-height: 1.75;
  color: #cbd5e1;
}

/* Every <a> anywhere inside .nav */
.nav a {
  text-decoration: none;
  color: #6366f1;
}

The key word in “descendant” is anywhere. It doesn’t matter how deep the element is nested. If a <p> is a direct child of .article, it gets selected. If it’s inside a <div> inside a <section> inside .article, it still gets selected.

That’s exactly why we call them descendants, not just children. The inheritance article talked about how some CSS properties flow from parent to child automatically. Combinators are the explicit version of that: you’re saying exactly which elements you want to reach into and style.

<div class="card">
  <h2>Card Title</h2>
  <p>Direct child paragraph.</p>
  <div class="card-body">
    <p>Deeply nested paragraph.</p>  <!-- also selected -->
  </div>
</div>
/* Selects BOTH paragraphs above */
.card p {
  font-size: 0.9rem;
  color: #cbd5e1;
}

04The CSS Child Combinator (>): Direct Children Only

The child combinator is the stricter version. Instead of “anywhere inside,” it means only direct children. One level deep. Nothing deeper.

/* Only <p> that are DIRECT children of .card */
.card > p {
  font-size: 0.9rem;
  color: #cbd5e1;
}

Using the same HTML above, .card > p would only select “Direct child paragraph.” The paragraph inside .card-body wouldn’t be touched because it’s a grandchild of .card, not a direct child.

Here’s where the child combinator really shines in practice, especially with navigation menus:

/* Style only the top-level list items */
nav > ul > li {
  display: inline-block;
  padding: 0 1rem;
}

/* This would also catch nested dropdown items - probably not what you want */
nav li {
  display: inline-block;
}

If your navigation has a dropdown submenu with its own nested <ul> and <li> elements, using the descendant selector would accidentally style those too. The child combinator keeps things precise.

Descendant vs Child Combinator: The Visual Difference

This is the one that confuses people the most, so let me show you both in action side-by-side. The demo below shows the same HTML structure, and you can click each combinator button to see exactly which elements would get selected:

Note: The button interactivity below uses a small JavaScript snippet to highlight elements visually. Don’t worry about that code for now. JavaScript is covered in detail in its own series. The CSS selectors themselves are pure CSS.

Click a combinator:



HTML Structure
<div class=“wrapper”>  <h2>Section Title</h2>  <p>Paragraph 1</p>  <div class=“nested”>    <p>Paragraph 2</p>    <p>Paragraph 3</p>  </div>  <p>Paragraph 4</p></div>
Visual Output
h2 Section Title
p Paragraph 1
p Paragraph 2
p Paragraph 3
p Paragraph 4

Try clicking each button and notice how the selection changes. The descendant selector catches all four paragraphs. The child selector only gets the ones sitting directly inside .wrapper. The adjacent sibling only gets the one paragraph directly after h2. The general sibling gets all paragraph-level siblings of h2.


05Sibling Selectors: Targeting What Comes After

Both sibling combinators work horizontally in the DOM. They look sideways, not up or down. They target elements at the same level, with the same parent.

One thing both have in common: they only look forward. CSS has no way to select a sibling that comes before an element. Sibling selectors always move in one direction: the elements that follow.

The Adjacent Sibling Selector (+): Immediate Neighbors Only

The + combinator targets one specific element: the very next sibling, and only if it matches the second selector.

/* Only the FIRST paragraph immediately after an h2 */
h2 + p {
  font-size: 1.05rem;
  color: #94a3b8;
  margin-top: 0.25rem;
}

/* Input that comes directly after a label */
label + input {
  display: block;
  margin-top: 0.4rem;
}

That first example is one I use all the time. The paragraph right after a heading is often a subtitle or intro line, and it deserves slightly different styling than the paragraphs that come later. The adjacent sibling selector is perfect for that.

If the very next element after h2 is a <div> and not a <p>, the rule doesn’t apply to anything. The elements have to match, and they have to be immediately adjacent.

The General Sibling Selector (~): All Siblings That Follow

The ~ combinator is more relaxed. It selects all matching elements that come after the first selector, as long as they’re siblings (same parent).

/* ALL paragraphs that follow an h2, as long as they share the same parent */
h2 ~ p {
  padding-left: 1rem;
  border-left: 2px solid rgba(99, 102, 241, 0.4);
}

/* A classic CSS-only trick: show a menu when a checkbox is checked */
input[type="checkbox"]:checked ~ .menu {
  display: block;
}

That second example is a classic trick you’ll come across. It uses the general sibling selector to show or hide an element based on a checkbox’s state, without touching any JavaScript. We’ll cover the :checked part in the pseudo-classes article, but it’s good to see the sibling selector powering a real interaction like that.

Important Distinction

h2 ~ p does NOT select every paragraph on the page that comes after an h2. It only selects paragraphs that are siblings of that h2, meaning they share the same direct parent. Paragraphs nested inside other elements don’t count as siblings of h2, even if they visually appear below it.

Adjacent vs General Sibling: A Clear Visual

Here’s the same structure with both selectors applied side-by-side:

h2 + p  (Adjacent Sibling)
h2 Section Title
p Paragraph 1 ✓ selected
p Paragraph 2
p Paragraph 3
Only the immediately next sibling matches.
h2 ~ p  (General Sibling)
h2 Section Title
p Paragraph 1 ✓ selected
p Paragraph 2 ✓ selected
p Paragraph 3 ✓ selected
All following siblings that match are selected.

06Mixing Grouped Selectors with Combinators

These two techniques work well together. You can group combinator-based selectors with a comma just like you’d group any other selectors.

/* Apply shared margin to direct-child headings across both elements */
.card > h2, .card > h3 {
  font-weight: 600;
  margin-bottom: 0.5rem;
  color: #ffffff;
}

/* Apply the same styles to paragraphs in both article and post */
.article > p, .post > p {
  margin-bottom: 1.25rem;
  line-height: 1.75;
  color: #cbd5e1;
}

/* Remove list styles from direct-child lists in nav and footer */
nav > ul, footer > ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

This is where CSS starts feeling genuinely efficient. One compact rule, precise targeting, shared styles across multiple elements or sections. This is the kind of CSS that’s easy to read and maintain six months later too.


07A Real Stylesheet Example Using All Four Techniques

Let’s put everything together with a real life card component. Here’s the HTML and the CSS that styles it using grouping, descendant, child, adjacent sibling, and general sibling selectors:

CSS
/* All paragraphs anywhere inside .card */
.card p {
  color: #cbd5e1;
  font-size: 0.875rem;
  line-height: 1.7;
}

/* Only the heading that's a direct child */
.card > h2 {
  font-size: 1.3rem;
  color: #ffffff;
  margin-bottom: 0.5rem;
}

/* First paragraph immediately after h2 */
h2 + p {
  font-style: italic;
  color: #94a3b8;
}

/* Spans after the first span in .card-meta */
span ~ span {
  margin-left: 1rem;
  padding-left: 1rem;
  border-left: 1px solid rgba(255,255,255,0.1);
}
Live Result

08Understanding CSS Specificity

A quick intro to how the browser decides which rules win.

When two rules target the same element, the one with higher specificity takes precedence. It’s a scoring system built into the cascade.

April 20256 min readIntermediate

Notice how each combinator handles a different relationship: the descendant selector reaches all paragraphs broadly, the child combinator locks the heading styling to direct children only, the adjacent sibling picks out the intro paragraph after the heading, and the general sibling creates that divider between the meta tags.

If you want to dig deeper into how those specificity scores are calculated when combinators are involved, the CSS cascade article covers all of that in detail.


09Common Pitfalls with CSS Combinators

A few things that tend to trip people up when they first start using combinators:

⚠ Mistake 1: Confusing Descendant and Child

Using a space when you meant > (or vice versa) is incredibly easy to do. If you use nav li and your nav has a dropdown submenu, you’ll accidentally style those nested items too. Use nav > li when you specifically mean top-level only.

⚠ Mistake 2: Thinking Sibling Selectors Look Backwards

CSS sibling selectors only move forward. h2 + p selects the <p> that comes after h2, never the one before it. There is no “previous sibling” selector in CSS today.

⚠ Mistake 3: Chaining Too Many Descendant Selectors

Writing .page .content .card .body p is technically valid but it’s fragile and hard to maintain. If you rename any one of those parent classes, the whole rule breaks. Keep selector chains short, ideally 2-3 levels deep at most.

⚠ Mistake 4: Forgetting Siblings Must Share a Parent

The general sibling selector (~) only targets elements with the same direct parent. Two paragraphs that visually appear at the same level in the rendered page but live under different parent elements in the HTML are not siblings, and the sibling selector won’t reach across that boundary.


10CSS Combinator Quick Reference

Here’s a summary of all four combinator types, their symbols, and what they do:

Name Symbol What It Selects Example
Descendant (space) Any matching element nested inside, at any depth .card p
Child > Direct children only, one level deep .card > p
Adjacent Sibling + The immediately following sibling element h2 + p
General Sibling ~ All following siblings that match h2 ~ p

11What’s Coming Next

We’ve covered grouping and all four combinators. Your selector toolkit is genuinely growing now, and these techniques will show up in almost every stylesheet you write.

The next article goes even further into selector precision: attribute selectors. These let you target elements based on the HTML attributes they have, not just their tags or classes. Things like selecting all links with href pointing to an external site, or styling inputs based on their type value, all without adding extra classes to your HTML. It’s one of the most underused parts of CSS, and it’s powerful.

Keep writing CSS. The more you use combinators in real projects, the more natural they feel, and the more you’ll start to see the HTML structure as the map your selectors are navigating.

Grouping and Combining Selectors

Choose a difficulty to load your challenge

Loading challenge…

Ready to Practice?

Select a difficulty level above to load your challenge.

Easy

1
Live Preview
Great Work!