Module 2: Selectors & Combinators
Combinators: Descendant, Child, Adjacent and General Sibling
How to select elements by their relationship to other elements, with the DOM tree in mind, and the limits of what combinators can reach.
Basic selectors say what an element is. Combinators say where it is, relative to other elements. To use them well you have to picture the DOM as a tree, because every combinator walks that tree in a specific direction.
<nav class="menu">
<h2>Sections</h2>
<ul>
<li><a href="/docs">Docs</a>
<ul>
<li><a href="/docs/api">API</a></li>
</ul>
</li>
<li><a href="/blog">Blog</a></li>
</ul>
<p>Updated daily</p>
</nav>
nav.menu
├── h2
├── ul
│ ├── li ── a(Docs)
│ │ └── ul
│ │ └── li ── a(API)
│ └── li ── a(Blog)
└── p
Descendant: a space#
.menu a { color: #0b5fff; }
Matches any a that has .menu somewhere above it, at any depth. In the tree above, all three links match. It is the most-used combinator, and the most careless. When you only wanted the top-level links, nested ones got caught too.
Child: >#
.menu > ul > li > a { font-weight: 700; }
Matches only direct children. Here only "Docs" and "Blog" are bold. "API" is a grandchild of the first li, so the chain does not reach it.
Use the child combinator when your structure is nested and you need to style one level without leaking into the next. Menus, comment threads and nested lists are the common cases.
Adjacent sibling: +#
h2 + p { margin-top: 0; }
Matches an element that comes immediately after another one, with the same parent. In the example, h2 + ul matches the ul, but h2 + p matches nothing, because the p comes after the ul, not directly after the h2.
The most famous use is spacing between repeated elements:
.stack > * + * {
margin-top: 1rem;
}
This reads as "any child that has a sibling right before it". The first child gets nothing, every other child gets a top margin. No extra space at the start, no :first-child override needed. People call it the lobotomized owl.
General sibling: ~#
h2 ~ p { color: #555; }
Matches every p that comes after an h2 among the same parent's children, not just the next one. It is useful with form controls, where a checkbox can restyle later siblings:
<input type="checkbox" id="toggle">
<label for="toggle">Show details</label>
<div class="details">…</div>
.details { display: none; }
#toggle:checked ~ .details { display: block; }
That is a working show-and-hide with zero JavaScript. It relies on the ~ combinator and the fact that the .details element comes after the checkbox.
The direction rule#
Combinators only look down the tree (descendants, children) and forward in document order (siblings that come after). They cannot select a parent or a previous sibling.
/* None of these are possible with combinators: */
/* "the li that CONTAINS an a.active" */
/* "the label that comes BEFORE the input" */
For years you worked around this with extra classes or JavaScript. Modern CSS added :has(), which can express both (Module 9). For now, understand that the limitation is real, and that it shaped how HTML and CSS were written.
Choosing between them#
| Need | Combinator |
|---|---|
| Any element inside a component | descendant (space) |
| One level only, safe for nesting | child > |
| Style the element right after something | adjacent + |
| Style everything after something | general ~ |
Common mistakes#
- Using descendant where you meant child. Nested menus and lists get double-styled. Symptoms: indentation or font weight spreading into deeper levels.
- Long chains.
.page .content .article .body p ais fragile and has high specificity. If the HTML changes, the rule silently stops working. - Expecting
+to skip elements. It needs the very next sibling. Whitespace and text do not count, but any element in between does. - Expecting
~to look backwards. It will not. Reorder the HTML, or use:has()if you can. - Using
>when the markup changes. Wrapping an element in a newdivbreaks every child-combinator chain that used to reach it. Child selectors are precise, and precise means brittle if your structure moves.
Practice#
- Using the menu markup above, color only the top-level links using the child combinator. Then do the same with a different approach using a class, and note which is more robust.
- Build a vertical stack of a heading, a paragraph and a button, and space them using only
> * + *. - Build a pure CSS accordion: a hidden checkbox, a label and a content panel that opens with
:checked ~. - Write a rule using
~that styles every paragraph after the firsth2in an article. Then try to write the reverse (paragraphs before theh2) and explain why you cannot.