Topic 1.2
Combinators
In one line
Combinators join two selectors by a relationship: a space means "anywhere inside", > means "directly inside", + means "the very next sibling" and ~ means "any later sibling".
Think of it like this
A family tree. "Grandma's descendants" means her children, grandchildren and great-grandchildren (the space). "Grandma's children" means only the first generation (>). "The sister born right after Ravi" is one person (+). "All the sisters born after Ravi" can be several people (~).
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Tree
- The shape of an HTML page: elements inside elements, like branches on a tree.
- Parent
- The element that directly contains another element.
- Child
- An element directly inside another element, one level down.
- Descendant
- Any element inside another element, at any depth: child, grandchild and so on.
- Sibling
- Elements that share the same parent, like brothers and sisters.
- Combinator
- A symbol between two selectors that says how the elements are related: space,
>,+or~. - Compound selector
- Selectors joined with no space that all describe one element, like
p.note.
Step by step
01See the page as a tree
Before choosing a combinator, picture the tree. Here an article has five children: an h2, two p, a div and one more p. The div has its own p child, which is a grandchild of the article.
<article class="post">
<h2>Title</h2>
<p>First paragraph.</p>
<p>Second paragraph.</p>
<div class="aside">
<p>Nested paragraph.</p>
</div>
<p>Last paragraph.</p>
</article>02Descendant: anywhere inside
.post p matches all four paragraphs, including the nested one, because each one is somewhere inside the article.
.post p {
color: #444;
}03Child: one level down only
.post > p matches First, Second and Last. The nested paragraph's parent is the div, not the article, so it is skipped.
.post > p {
line-height: 1.6;
}04Next sibling: the one right after
h2 + p matches only First: it is the element immediately after the h2. A common use is styling a lead paragraph under a heading.
h2 + p {
font-size: 1.15em;
font-weight: 600;
}05Later siblings: every one after
h2 ~ p matches First, Second and Last: every p that shares the h2's parent and comes after it. The nested paragraph is not a sibling (different parent), so it is skipped.
Read any complex selector from right to left: "a p… that comes after… an h2". That's also how the browser checks it.
h2 ~ p {
margin-left: 12px;
}Try it yourself
- 1
Predict the nested paragraph
In the first example, change
.post > pto.post p. Predict: will the nested paragraph get the blue bar? (Yes.) Run it and check. - 2
Break the adjacency
In the second example, add
<hr>between<h2>Title</h2>and<p>First</p>. Predict whath2 + plogs now. (An empty array[], because thepis no longer immediately after theh2.) - 3
Dividers between items
In the menu example, add
.menu li li + li { border-top: 1px dashed #ccc; }. Predict which items get a dashed line: only CSS, the second nested item.
Code & diagrams
Only the nested paragraph has no blue bar and no blue background: it's a descendant of the article but not its child or the h2's sibling.
Preview
Console
Nothing logged yet.`p + p` matches Second only: Last comes right after a `div`, not a `p`.
Preview
Console
Nothing logged yet.Expected console output
.post p [ 'First', 'Second', 'Nested', 'Last' ]
.post > p [ 'First', 'Second', 'Last' ]
h2 + p [ 'First' ]
h2 ~ p [ 'First', 'Second', 'Last' ]
p + p [ 'Second' ]Change `.menu > li` to `.menu li` and remove the second rule: the sub-menu items become bold too, because `font-weight` reaches every descendant `li`.
Preview
Console
Nothing logged yet.Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Descendant styles leak into nested components
In the menu example, replace both li rules with one rule: .menu li { font-weight: 700; }.
Break #2
Expecting `~` to look backwards
Write p ~ h2 { color: red; } hoping to colour the heading that comes before the paragraphs.
Myth vs fact
Myth
div p only matches paragraphs that are direct children of a div.
Fact
A space means any depth. Direct children need div > p.
Myth
h2 + p matches all paragraphs after a heading.
Fact
+ matches only the single element immediately after. Use ~ for every later sibling.
Myth
Long descendant chains like body div.main ul li a make CSS more precise.
Fact
They make it fragile: any change to the HTML structure breaks the selector, and the specificity grows. A single class such as .nav-link is clearer and survives refactors.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Because matching runs right to left,
.post > pchecks eachp's parent once, while.post pmay walk all the way up the tree for everypthat isn't in a post. With today's engines (which cache ancestor information with Bloom filters) this rarely matters, but deep descendant chains with a broad key selector like*are still the slowest kind. - ▸
Sibling combinators are expensive to invalidate: when an element is inserted, a
~rule can force the browser to re-check every later sibling. Large lists that change often are where this shows up in performance profiles. - ▸
In Selectors Level 4 the names changed: the spec now says next-sibling combinator (
+) and subsequent-sibling combinator (~). Older articles say adjacent and general; they mean the same thing. - ▸
A column combinator
||(for table columns) exists in the spec but no browser ships it. Don't use it.
Remember this
- 1
HTML is a tree: elements sit inside other elements. The outer element is the parent, the elements directly inside it are its children, elements at any depth inside are its descendants, and elements that share the same parent are siblings. A combinator is a symbol between two selectors that describes how the two elements are related in that tree.
- 2
The descendant combinator is a space:
.post pmatches everypanywhere inside an element with classpost, at any depth: a child, a grandchild, deeper. It is the most common combinator and the easiest to over-use, because it reaches into nested components you didn't mean to style. - 3
The child combinator is
>:.post > pmatches onlypelements whose parent is the.post. A paragraph inside adivinside the post is not matched. Use it when nested structures (menus inside menus, cards inside cards) must not inherit the outer styles. - 4
The next-sibling combinator (also called the adjacent sibling combinator) is
+:h2 + pmatches apthat comes immediately after anh2with the same parent. Only one element can match perh2. Text and comments in between are ignored, but any other element in between breaks the match. A classic use:li + li { border-top: 1px solid #ddd; }draws a line between items but not above the first. - 5
The subsequent-sibling combinator (also called the general sibling combinator) is
~:h2 ~ pmatches everypthat comes after anh2with the same parent, not only the next one. Siblings before theh2are never matched. - 6
Combinators only look down (to descendants) or forward (to later siblings). There is no combinator for a parent or an earlier sibling. Modern CSS fills that gap with
:has()(Topic 1.5):.card:has(> img)selects a card by what is inside it. Combinators themselves add nothing to specificity; only the selectors they join count.
Explain it without notes
Explain the four combinators with one example each.
What is the difference between .card p and .card > p, and when does it matter?
Why can't a combinator select a parent or a previous sibling, and what does CSS offer instead?
Practice
In a <ul class="steps">, give every list item except the first a top margin of 8px, using a combinator.
Make the paragraph immediately after every h3 italic, and every later paragraph in the same parent indented by 16px.
Trade-offs
- ↔
The child combinator is safer for nested components but ties your CSS to the exact HTML structure; if someone wraps the element in a
div, the rule stops matching. A dedicated class avoids both problems. - ↔
+and~let you style by position without adding classes, but they break silently when markup is reordered.
Done when you can
I can describe a page as a tree with parents, children, descendants and siblings.
I can choose between a space and
>for nested structures.I can use
+and~and know they only look forward.I can read a complex selector from right to left.
I know that combinators add nothing to specificity.