CSS Combinators: Descendant, Child, and Sibling Selectors

Published Updated

A combinator is the symbol between two selectors that tells the browser how the elements should be related before the rule applies.

Syntax

.parent .descendant { /* space: any descendant, nested at any depth */ }
.parent > .child     { /* >: direct child only */ }
.item + .next        { /* +: the very next sibling */ }
.item ~ .later       { /* ~: any following sibling */ }

The Descendant Combinator

A plain space between two selectors matches the second element when it sits anywhere inside the first, no matter how deeply it is nested. This is the combinator you will use most, because it lets you scope styles to a region without touching the same elements elsewhere:

.article a {
  text-decoration: underline;
}

That rule underlines every link inside .article, whether the link sits directly in a paragraph or several levels down in a list. The reach is wide, so keep the left side specific enough that you do not accidentally style links in some unrelated corner of the page.

Because the descendant combinator does not care about depth, it also does not care about intervening elements. .article a matches a link tucked inside three nested divs just as readily as one sitting directly in the article, which is convenient until a component you did not expect gets nested inside .article and quietly inherits styles meant for something else.

The Child Combinator

The > symbol tightens things up by matching only direct children of the first element. Nesting no longer leaks your styles downward, which matters a lot for menus and nested lists:

.menu > li {
  border-bottom: 1px solid var(--color-border);
}

Here only the top-level list items get a border. If a .menu item contains its own sub-list, those inner items stay untouched, because they are grandchildren rather than direct children.

The child combinator earns its keep in exactly the situation where the descendant combinator overreaches. A nested .menu used for a dropdown, for instance, will not pick up the parent menu's border because > only ever looks one level down. That containment is what makes > the safer default once a component might end up nested inside a copy of itself.

The Sibling Combinators

The + combinator matches the element immediately after another at the same level, and the ~ combinator matches every following sibling. Both are handy for spacing and for reacting to a neighbor's state:

h2 + p {
  margin-top: 0;
}

.error ~ .field {
  opacity: 0.6;
}

The first rule removes the top margin from a paragraph that directly follows an h2, tightening the gap under a heading. The second dims every .field that comes after a .error element in the same parent.

Both sibling combinators only look forward and only within the same parent, so .item + .next cannot reach across into a different list even if the markup looks similar. The + version is the one you will reach for most, since spacing adjustments almost always care about exactly one neighbor rather than every element that follows.

The Gotcha: Combinators Never Look Upward or Backward

A combinator can reach down into descendants or forward to later siblings, but it can never select a parent or an earlier sibling. There is no "previous sibling" symbol and no parent combinator in this family, so you cannot style an element based on something that comes after it in the markup. When you find yourself wanting that, reorder the HTML, move the class up to a shared ancestor, or look at the :has() relational pseudo-class instead. :has() is the newest tool in this family, and it works by testing what a descendant looks like rather than testing position, so it solves a different kind of problem than a plain combinator ever could.

Example

.recipe > h3 {
  margin-block: 1rem 0.25rem;
}

.recipe h3 + ul {
  margin-top: 0;
}
<section class="recipe">
  <h3>Ingredients</h3>
  <ul>
    <li>Flour</li>
    <li>Water</li>
  </ul>
</section>

The .recipe > h3 rule spaces the heading because it is a direct child of .recipe. The h3 + ul rule pulls the list up tight against that heading because the list is the immediate next sibling. Together they give you controlled spacing without adding a single utility class.

Frequently Asked Questions

Can you mix a child and a descendant combinator in one selector?

Yes, and it is common: a direct child, then any depth beneath it. Each combinator applies only between the two parts it sits between, so read the selector left to right and treat every symbol as a separate relationship.

Is there a parent selector in CSS?

Effectively yes, through :has(). It lets a rule match an element based on what it contains, such as a card that holds an image. It is supported across current browsers, so the old answer of no longer applies.

How many combinators can you chain in one selector?

There is no limit, but each one ties the rule more tightly to the current markup. Selectors more than about three levels deep tend to break when a wrapper is added, which is an argument for a class on the element you actually want.

Do combinators work with pseudo-classes?

Yes, and combining them is common: a child combinator with :first-child, or a sibling combinator with :checked to style a label next to a ticked input. The combinator sets the relationship and the pseudo-class narrows it by state or position.

Do sibling combinators require both elements to share a parent?

Yes. Both the adjacent and general sibling combinators only match elements with the same parent. Two elements that look adjacent on screen but sit in different containers are not siblings, which is the usual reason a sibling rule silently fails.

Sources

  1. [1]
    Descendant combinator
    (developer.mozilla.org)
  2. [2]
    Child combinator
    (developer.mozilla.org)
  3. [3]
    Next-sibling combinator
    (developer.mozilla.org)
  4. [4]
    :has()
    (developer.mozilla.org)