CSS Combinators
Learning outcomes:
- What are combinators
- The descendant combinator —
- The child combinator —
>
- The next-sibling combinator —
+
- The subsequent-sibling combinator —
~
- Combining combinators together
Introduction
Let's say you're asked to select all <p>
elements in an HTML document that are within the <main>
element. What selector would you define for this?
Well, it turns out that we need to know about combinators first before we can tackle such a problem. This chapter is all about exploring combinators in CSS.
We'll learn about the utility of combinators alongside compound selectors in CSS, and then go through all four of the combinators in CSS — descendant (
), child (>
), next-sibling (+
), and subsequent-sibling (~
).
Not only this but we'll also take a look over many examples of using each of these combinators in selecting elements in a multitude of ways, with a multitude of conditions imposed.
Without further ado, let's get into it!
What are combinators?
CSS selectors already feature a great deal of strength in themselves to select HTML elements in numerous ways.
As we've seen uptil this point in this course, we can base the selection on the types of HTML elements, their IDs, classes, attributes (only existing or with given values), states (such as :hover
), or additional content.
However, the potential for element selection in CSS doesn't end here; we have something called combinators giving another push to this superbly powerful selection system.
So what exactly is a combinator?
In simple words, a combinator is just a special character that sits between two different compound selectors in CSS to literally 'combine' the two selectors into one.
The semantics of the combination depend on the combinator being used.
For example, using the descendant combinator (
, denoted as a space) between two compound selectors, we refer to all elements that fulfill the selector on the right-hand side and are descendants of elements selected by the left-hand side selector.
Similarly, using the child combinator (>
) instead, we refer to all elements that fulfill the selector on the right-hand side but are direct children of the elements selected by the left-hand side selector.
The general format of a combinator is depicted as follows:
<selector><combinator><selector>
It sits right between two compound selectors; it's a must to have exactly one compound selector on either of its sides.
The selector obtained by combining two compound selectors using a combinator is known as a complex selector.
There are a total of 4 combinators in CSS, as shown below:
Symbol | Name | General form | Meaning |
---|---|---|---|
(space) | Descendant | ab | Selects all elements b that are descendants of a . |
> | Child | a > b | Selects all elements b that are children of a . |
+ | Next-sibling | a + b | Selects all elements b that are next siblings of a . |
~ | Subsequent-sibling | a ~ b | Selects all elements b that are subsequent siblings of a . |
They are all really simple to understand so let's consider each one turn by turn.
The HTML example
To illustrate the differences between the different combinators, we'll use the following HTML code:
<body>
<p>Paragraph 1.1</p>
<section>
<p>Paragraph 2.1</p>
<div>
<p>Paragraph 3.1</p>
<p>Paragraph 3.2</p>
<p>Paragraph 3.3</p>
</div>
<p>Paragraph 2.2</p>
</section>
<p>Paragraph 1.2</p>
<p>Paragraph 1.3</p>
</body>
Basically, we have a bunch of <p>
s here with different nesting levels. Some are directly within <body>
, whereas some are wrapped up in other elements, <section>
and <div>
.
Most importantly, notice the numbering of each paragraph's text:
- The first number indicates its nesting inside
<body>
.1
means directly inside<body>
,2
means directly inside another element that's directly inside<body>
, and so on. - The second number indicates the position of the
<p>
in its parent.
To better distinguish the <section>
and <div>
containers, we'll apply the following CSS:
section {
border: 3px solid blue;
}
div {
border: 3px dashed red;
}
Here's the output:
Paragraph 1.1
Paragraph 2.1
Paragraph 3.1
Paragraph 3.2
Paragraph 3.3
Paragraph 2.2
Paragraph 1.2
Paragraph 1.3
To even further mark the <section>
as a <section>
and the <div>
as a <div>
, we can leverage a concept that we learned back in the previous chapter — assign a label to these elements using the ::before
pseudo-element:
section::before {
content: 'Section';
color: blue;
}
div::before {
content: 'Div';
color: red;
}
Paragraph 1.1
Paragraph 2.1
Paragraph 3.1
Paragraph 3.2
Paragraph 3.3
Paragraph 2.2
Paragraph 1.2
Paragraph 1.3
Perfect! Now this is the base on which we'll experiment with all 4 combinators in the following sections.
Descendant combinator
Perhaps the simplest of all combinators is the descendant combinator. It's denoted as a
(space) character.
A
and B
such that AB
matches all elements matched by the selector B
that are descendants of elements matched by the selector A
.An element B
is a said to be a descendant of an element A
if and only if B
is nested inside A
.
It's worthwhile taking a quick break here to first understand what is meant by the descendants of an element and then resume learning about combinators.
Family-like relationships of HTML elements
Have you ever seen a family tree where we start with a given person and then write down all his/her children, followed by writing down all the children of each of those children, and so on?
Because HTML markup, by virtue of its nesting, also forms a tree-like structure, it's helpful to refer to the relationships between different elements in the same way as family relationships.
Let's get to it...
To begin with, consider the following HTML:
<main>
<h1>The main heading</h1>
<div>
<p>A paragraph</p>
</div>
</main>
The <main>
element here has two children: <h1>
and <div>
. This is because it's these elements that come directly inside<main>
.
<p>
element is NOT a child of <main>
.The parent of the <h2>
is <main>
and so is the parent of <div>
.
Because both <h1>
and <div>
are within the same element, i.e. have the same parent, they are siblings of each other.
Applying this logic to the <div>
and <p>
elements: <div>
is the parent of <p>
; <p>
is the child of <div>
; and <p>
has no siblings (it's the only child of its parent).
Following family-tree relationships, we can even define the terms grandparent and grandchild here. That is, the grandparent of <p>
is <main>
. Similarly, <p>
is a grandchild of <main>
, via <div>
.
And speaking in general terms, the descendants (children, grandchildren, great-grandchildren, etc.) of <main>
are <h2>
, <div>
, and <p>
.
On the same lines, the ancestors (parent, grandparent, great-grandparent) of the <p>
are <div>
and <main>
, in this very order.
Coming back to the discussion, consider the following CSS:
p {
background-color: yellow;
}
The selector p
(a type selector) would obviously match all <p>
elements on the document — nothing really interesting there.
But now consider the following selector:
section p {
background-color: yellow;
}
This time, we're only concerned with those <p>
elements that are within <section>
elements — in other words, those <p>
elements that are the descendants of <section>
elements.
The section p
selector selects all <p>
s elements within <section>
:
Paragraph 1.1
Paragraph 2.1
Paragraph 3.1
Paragraph 3.2
Paragraph 3.3
Paragraph 2.2
Paragraph 1.2
Paragraph 1.3
Note how the <p>
s within the <div>
that is inside <section>
are also selected. This is because <section p>
refers to all <p>
elements inside <section>
no matter where they occur within it.
Based on the descendant combinator and ideas learned in the previous chapters, try answering the following question:
Which paragraphs would the selector section p:first-child
select?
- Paragraph 2.1
- Paragraph 3.1
- Paragraph 2.1 and Paragraph 3.1
- Nothing
section p:first-child
selects all first children <p>
elements that are within the <section>
element. These are the ones that read "Paragraph 2.1" and "Paragraph 3.1".
Here's a live example:
Child combinator
The child combinator is essentially a restricted version of the descendant combinator.
>
) combines two selectors A
and B
such that A > B
matches all elements matched by the selector B
that are children of elements matched by the selector A
.Recall that an element B
is said to be the child of an element A
if B
is directly nested inside A
.
Let's see what does section > p
match in our example document:
section > p {
background-color: yellow;
}
Paragraph 1.1
Paragraph 2.1
Paragraph 3.1
Paragraph 3.2
Paragraph 3.3
Paragraph 2.2
Paragraph 1.2
Paragraph 1.3
As can be seen, only "Paragraph 2.1" and "Paragraph 2.2" are matched because only they are the children of <section>
.
As before, it's quiz time...
Which paragraphs would the selector body > p
select?
- Paragraph 1.1
- Paragraph 1.2
- Paragraph 1.1, Paragraph 1.2, and Paragraph 1.3
- All paragraphs
body > p
selects all children <p>
elements of the <body>
element, which happen to be the ones that read "Paragraph 1.1", "Paragraph 1.2", and "Paragraph 1.3."
Here's a live example:
If you have a hard time remembering whether the child combinator is denoted as >
or <
, here's a simple way to remember it:
In a family tree, you move from a parent to a child, right? Likewise, use the symbol that gives the same impression, that is, seems to make the move from the left selector (the parent) to the right one (the child). Basically we ought to go from left to right and that's what >
does.
Easy way to remember, isn't it?
Next-sibling combinator
The next-sibling combinator, as per its name, places the next-sibling semantics on the selector following it.
+
) combines two selectors A
and B
such that A + B
matches all elements matched by the selector B
that are next siblings of elements matched by the selector A
.The next sibling of an element A
is simply the very next sibling element B
that comes after A
.
Let's see what gets selected by section + p
in our example and then see why is that so:
section + p {
background-color: yellow;
}
Paragraph 1.1
Paragraph 2.1
Paragraph 3.1
Paragraph 3.2
Paragraph 3.3
Paragraph 2.2
Paragraph 1.2
Paragraph 1.3
section + p
selects all <p>
elements that come right after<section>
elements. In our case, the <p>
that abides by this rule is only "Paragraph 1.2."
"Paragraph 1.3" doesn't come immediately after the <section>
, hence it's not matched by section + p
.
Which paragraphs would the selector p + p
select?
- Paragraph 1.1 and Paragraph 1.2
- Paragraph 2.1 and Paragraph 2.2
- Paragraph 1.3, Paragraph 3.2, and Paragraph 3.3
p + p
selects all <p>
elements that appear right after <p>
elements. This happens to apply to the <p>
elements that read "Paragraph 1.3", "Paragraph 3.2", and "Paragraph 3.3."
Here's a live example:
Subsequent sibling combinator
The subsequent-sibling combinator is a more generous extension of the next-sibling combinator.
~
) combines two selectors A
and B
such that A ~ B
matches all elements matched by the selector B
that are subsequent siblings of elements matched by the selector A
.The subsequent siblings of an element A
are all the siblings that come afterA
.
Let's replace the next-sibling combinator in the selector section + p
with the subsequent-sibling combinator and see the change it produces:
section ~ p {
background-color: yellow;
}
Paragraph 1.1
Paragraph 2.1
Paragraph 3.1
Paragraph 3.2
Paragraph 3.3
Paragraph 2.2
Paragraph 1.2
Paragraph 1.3
See how section ~ p
matches both the paragraphs after the <section>
element in contrast to section + p
which matched only the first one after <section>
.
This is simply because section ~ p
looks for all sibling <p>
elements that come after<section>
s; there's no condition to come right after<section>
(unlike with +
).
Which paragraphs would the selector p ~ p
select?
- Paragraph 1.2 and Paragraph 1.3
- Paragraph 3.2 and Paragraph 3.3
- Paragraph 1.3, Paragraph 3.1, and Paragraph 3.2
- Paragraph 1.2, Paragraph 1.3, Paragraph 2.2, Paragraph 3.2, and Paragraph 3.3
p ~ p
selects all <p>
elements that appear after <p>
elements. This happens to apply to the <p>
elements that read "Paragraph 1.2", "Paragraph 1.3", "Paragraph 2.2", "Paragraph 3.2", and "Paragraph 3.3."
Here's a live example:
Combining combinators
Now that we've learned about all combinators in CSS, it's the high time to consider the case when we combine selectors that contain combinators themselves.
For example, let's say we want to select all <li>
elements of <ol>
elements that appear inside the <main>
element in a document.
To accomplish this, we'll use the selector main ol li
. Notice the two different combinators being used here — one is the descendant combinator between main
and ol
while the other one is also a descendant combinator between ol
and li
.
Such a selector that is comprised of multiple combinators between multiple compound selectors is also referred to as a complex selector.
The formal definition of a complex selector
As per the current specification of CSS selectors, a complex selector is defined as a compound selector followed by zero or more combinator–compound-selector pairs.
In this way, a standalone compound selector, e.g. main
, is also a complex selector. And so is main ol
— a complex selector (comprised of a compound selector, followed by the descendant combinator, followed by another compound selector).
If this sounds a little bit technical and difficult to understand, the truth is that it is technical. Formal grammar specifications are always technical and tedious to work with and understand.
The way a complex selector containing multiple combinators is parsed and probed for matching subjects theoretically, like the one we just presented (i.e. main ol li
), is as follows:
In other words, resolution starts from the very left and goes to the right. Subjects are filtered subsequently from left to right based on the conditions imposed by selectors and combinators.
Let's take the example of main ol li
to better understand this.
- First,
main
gets resolved. Trivially, being just a type selector, it matches all<main>
elements. - Next comes
ol
(with the descendant combinator). This matches all<ol>
elements that are descendants of elements matched in 1). - Finally comes
li
. This matches all<li>
elements that are descendants of elements matched in 2).
Let's take another example. Consider the complex selector section#s1 p + h2
:
- First,
section#s1
gets resolved. It matches the<section>
element that has the ID"s1"
. - Next comes
p
(with the descendant combinator). This matches all<p>
elements that are descendants of elements matched in 1). - Finally comes
+ h2
. This matches all<h2>
elements that are the next-sibling of elements matched in 2).
As you can see here, the intuition is simple: keep resolving selectors and combinator-selector pairs from the left to the right, filtering down the list of matched elements from the ones obtained thus far.
These examples are great but let's also consider the example that we've been working on throughout this chapter and visualize exactly what gets selected by a given complex selector.
Take a look at the following code:
section p + * {
background-color: yellow;
border: 2px solid black;
}
Now before I present to you the output of this, spare a few minutes to determine this yourself and then see whether your reasoning was right or not. It's thinking time!
section p + *
select?Alright, let's see the output:
Paragraph 1.1
Paragraph 2.1
Paragraph 3.1
Paragraph 3.2
Paragraph 3.3
Paragraph 2.2
Paragraph 1.2
Paragraph 1.3
The elements that get selected are the <div>
, "Paragraph 3.2" and "Paragraph 3.3."
The dissection of section p + *
goes as follows:
section p
here first selects all<p>
elements that are within the<section>
.- Next,
+ *
selects all elements (courtesy of the universal selector,*
) that are the next siblings of these<p>
elements.
More specifically, section p
selects "Paragraph 2.1", "Paragraph 2.2", "Paragraph 3.1", "Paragraph 3.2", and "Paragraph 3.3."
Applying + *
on these elements, the next sibling of "Paragraph 2.1" is the <div>
(first match). "Paragraph 2.2" does not have a next sibling. The next sibling of "Paragraph 3.1" is "Paragraph 3.2." (second match). Similarly, the next sibling of "Paragraph 3.2" is "Paragraph 3.3" (third match). "Paragraph 3.3" does not have a next sibling.
And there we have our three subjects of section p + *
.
Was this easy or not?
As you start developing real websites, you'll deal with suchlike complex selectors more often than not. Likewise, it's a good idea to start practicing with these complex selectors more and more.
Spread the word
Think that the content was awesome? Share it with your friends!
Join the community
Can't understand something related to the content? Get help from the community.