Command Palette

Search for a command to run...

Hectal
PHASE 1Beginner ~19 min· topic 2 of 6

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.

index.htmlwhole filehtml
<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>
See the page as a treediagram
Rendering diagram…

02Descendant: anywhere inside

.post p matches all four paragraphs, including the nested one, because each one is somewhere inside the article.

style.csswhole filecss
.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.

style.csswhole filecss
.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.

style.csswhole filecss
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.

style.csswhole filecss
h2 ~ p {
  margin-left: 12px;
}

Try it yourself

  1. 1

    Predict the nested paragraph

    In the first example, change .post > p to .post p. Predict: will the nested paragraph get the blue bar? (Yes.) Run it and check.

  2. 2

    Break the adjacency

    In the second example, add <hr> between <h2>Title</h2> and <p>First</p>. Predict what h2 + p logs now. (An empty array [], because the p is no longer immediately after the h2.)

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

The four combinators on one tree New tab

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

Press Run to see the page.

Console

Nothing logged yet.
Which elements does each combinator match? New tab

`p + p` matches Second only: Last comes right after a `div`, not a `p`.

Preview

Press Run to see the page.

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' ]
Nested menus: descendant versus child New tab

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

Press Run to see the page.

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

terminal
$ the preview shows
── what you'll see ──
HTML and CSS in the sub-menu are bold too, so the nested level no longer looks different.

Break #2

Expecting `~` to look backwards

Write p ~ h2 { color: red; } hoping to colour the heading that comes before the paragraphs.

terminal
$ the preview shows
── what you'll see ──
The heading stays its normal colour.

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 > p checks each p's parent once, while .post p may walk all the way up the tree for every p that 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. 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. 2

    The descendant combinator is a space: .post p matches every p anywhere inside an element with class post, 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. 3

    The child combinator is >: .post > p matches only p elements whose parent is the .post. A paragraph inside a div inside the post is not matched. Use it when nested structures (menus inside menus, cards inside cards) must not inherit the outer styles.

  4. 4

    The next-sibling combinator (also called the adjacent sibling combinator) is +: h2 + p matches a p that comes immediately after an h2 with the same parent. Only one element can match per h2. 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. 5

    The subsequent-sibling combinator (also called the general sibling combinator) is ~: h2 ~ p matches every p that comes after an h2 with the same parent, not only the next one. Siblings before the h2 are never matched.

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

01

Explain the four combinators with one example each.

02

What is the difference between .card p and .card > p, and when does it matter?

03

Why can't a combinator select a parent or a previous sibling, and what does CSS offer instead?

Practice

01

In a <ul class="steps">, give every list item except the first a top margin of 8px, using a combinator.

02

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.