CSS Combinators – Advanced CSS Selectors – Part 3

In the previous tutorial, you have seen how to combine CSS selectors to precisely specify an HTML element for styling and the rules by which the specificity of a CSS selector is calculated.

You will learn about combinators for combining CSS selectors to more precisely select HTML elements for styling.

Contents

  1. Child Combinator
  2. Descendant Combinator
  3. Next Sibling Combinator
  4. Subsequent Sibling Combinator
  5. Closing Thoughts
  6. References

Child Combinator

You were introduced to the Child Combinator in the first part of this series on Advanced CSS Selectors:

.column-left > .card > .headline {
  background-color: lightgreen;
}

The combinator > (greater-than symbol1) specifies a direct parent-child relationship.

In the example above, it selects an element with the class of .headline only if it is an immediate child of an element with the class of .card AND the element with the class of .card is an immediate child of an element with the class of .column-left.

It has a specificity of 0:3:0.

Consider the following HTML code and the selector declared above:

<!--
  The <p> tag will be selected because the
  parent-child relationships match the selector.

  You could also use the selector:

  div > div > p

  That selector has a specificity of 0:0:3
-->
<div class="column-left">
  <div class="card">
    <p class="headline">A Headline</p>
  </div>
</div>

<!--
  The <p> tag will NOT be selected because the
  parent-child relationships do not match the
  selector

  To select the <p> tag, the selector could be:

  .column-left  > div > .headline

  That selector has a specificity of 0:2:1
-->
<div class="column-left">
  <div>
    <p class="headline">A Headline</p>
  </div>
</div>

<!--
  The <p> tag will NOT be selected because the
  parent-child relationships do not match the
  selector.

  To select the <p> tag, the selector could be:

  .card > .column-left  > .headline

  That selector has a specificity of 0:3:0
-->
<div class="card">
  <div class="column-left">
    <p class="headline">A Headline</p>
  </div>
</div>

<!--
  The <p> tag WILL be selected because the
  parent-child relationships match the selector.

  Both <p> tags are children of the parent with
  the class .card. The position of the child
  within the parent doesn't affect its selection.
  All that matters is that it is a direct child.
-->
<div class="column-left">
  <div class="card">
    <p>Some random text.</p>
    <p class="headline">A Headline</p>
  </div>
</div>

<!--
  The <p> tag will NOT be selected because the
  parent-child relationships do not match the
  selector.

  To select the <p> tag, the selector could be:

  .column-left > .card > div > .headline

  That selector has a specificity of 0:3:1

  It could also be:

  div > div > div > p

  That selector has a specificity of 0:0:4
-->
<div class="column-left">
  <div class="card">
    <div>
      <p class="headline">A Headline</p>
    </div>
  </div>
</div>

While I wrote the selector with a space around the combinator, there is no need for that. The following child selectors are also valid:

.column-left>.card.>.headline {

}

div>div>p {

}

While the direct selection method discussed in the previous tutorial can never have an #ID specificity greater than 1 (because an HTML element can never have more than 1 ID associated with it), with combinators, it is possible to have an #ID specificity greater than 1. Consider the following (based on the More Advanced float Example):

#wrapper > #title {
  /* specificity = 2:0:0 */
}

#wrapper > #navbar {
  /* specificity = 2:0:0 */
}

#wrapper > #content > .column-left > .card > .headline {
  /* specificity = 2:3:0 */
}

Descendant Combinator

This is the most commonly used combinator.

The descendant combinator is more general than the child combinator and is useful for styling different sections of your document without having to specify the exact parent-child relationships.

As you recall, when you create your HTML page, you divide the page into various sections. Common sections include:

  • Header
  • Navigation Bar
  • Main Body
  • Side Bar
  • Footer

You might like to have common styles within a section, but not have to specify the exact relationship for every element in a section, but give a more general “all p tags in the Main Body, regardless of how the main body is subdivided, have the following styling”.

Consider the clone page created in an earlier lesson, it had a #content section that contained three columns:

You can style ALL p elements in the document using:

/*
  applies to all <p> tags, unless
  overridden by a more specific selector
*/
p {
  /* specificity = 0:0:1 */
}

You can also style all p elements that are immediate children of the #content section using the child combinator:

/*
  Only applies to <p> elements that are
  immediate children of a #content element,
*/
#content > p {
  /* specificity = 1:0:1 */  
}

But the child combinator will not select p elements if they are nested deeper:


<div id="content">
  <p>This will be selected.</p>
  <div class="aside">
    <p>This will NOT be selected</p>
  </div>
</div>

The descendant combinator allows you to create a selector for ALL p elements located inside a #content container – you can select immediate children, grandchildren and more deeply nested elements. The descendant combinator is a space character:2

/*
  The descendant combinator is the space character.
*/
#content p {
  /* specificity = 1:0:1 */  
}

/*
  The space character acts as a descendant combinator
  when it is alone. If you write a child combinator
  with spaces it is a child combinator, not a
  child + descendant combinator.
*/
#content > p {
  /* specificity = 1:0:1 */
}

This top selector will select all the p elements in the HTML example above, but if they are located anywhere inside a block with an ID of #content.

The bottom selector will only select those the p elements which are immediate children of a block with an ID of #content.

This is useful for defining styles that are localized to particular sections of your webpage without having to rigidly follow the structure of your page:

#header p {
  /*
    applies to all <p> tags inside #header
    specificity = 1:0:1
  */
}

#navbar a{
  /*
    applies to all links inside #navbar
    specificity = 1:0:1
  */
}

#content .sidebar img {
  /*
    applies to all images located anywhere
    inside .sidebar, when .sidebar is
    located anywhere inside #content
    specificity = 1:1:1
  */
}

#content .aside > img {
  /*
    applies to images inside #content that
    are direct children of .aside
    specificity = 1:1:1
  */
}

#footer img {
  /*
    applies to all images inside #footer
    specificity = 1:0:1
  */
}

Remember to pay attention to the specificity of your selector:

#content p {
  /* specificity 1:0:1 */
}

.aside > p {
  /* specificity 0:1:1 */
}

/*
  Even though the bottom selector looks more "specific"
  than the top selector, because the top selector says
  "select any <p> found anywhere inside #content" and
  the bottom selector says "only select <p> if it is
  an immediate child of .aside".

  However, the calculated specificity of the top selector
  is greater than the specificity of the bottom selector.
*/

Next Sibling Combinator

This is also known as the first sibling combinator and adjacent sibling combinator.

The first two combinators allow you to select HTML elements based on how they are contained within other HTML elements – either as children, grandchildren, great-grandchildren, etc.

The next sibling combinator allows you to select HTML elements in relation to other elements at the same nesting level. Consider the following:

<!-- This is a parent element -->
<div>
  <!--
    The <h1>, <p>, <p>, <ol>, <img>, and <p>
    are siblings and children of <div>
  -->
  <h1>A Header</h1>
  <p>A paragraph.</p>
  <p>Another paragraph</p>
  <!--
    While <ol> is a child of <div> and a sibling
    to <h1>, <p>, <p>, <img>, and <p>, it is also
    the parent of the <li> elements.
  -->
  <ol>
    <!-- All the <li> elements are siblings -->
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
  </ol>
  <img src="/valid/image.jpg">
  <p>Maybe a caption?</p>
</div>

Consider the following rules using the next sibling combinator (which is the plus ‘+’ symbol):

/*
  Select a <p> element if it immediately follows
  an <h1> element.

  You might want the first paragraph following
  <h1> to be special in some way - perhaps a
  slightly larger font or italicized. This could
  also be done by applying a class to the first
  <p> element.
*/
h1 + p {
  /* specificity = 0:0:2 */
}

/*
  Select a <p> element if it immediately follows
  an <img> element.

  You might use the convention that each image
  in your documents must be followed by a caption.
  You could also apply a class (like .caption) to
  the <p> element instead.
*/
img p {
  /* specificity = 0:0:2 */
}

/*
  Select an <li> element if it immediately follows
  an <li> element.

  Applied to the HTML code above, it will
  select the <li>s containing 'Item 2' and
  'Item 3' - this is because:
  - <li>Item 1</li> is the first li in the
    rule and <li>Item 2</li> is the second
    li in the rule - the one that is
    selected.
  - then <li>Item 2</li> is the first li in
    the rule and <li>Item 3</li> is the second
    li in the rule - the one that is
    selected.
  - when <li>Item 3</li> is evaluated as the
    first li in the rule, because there is
    no subsequent li, this selector isn't
    applied to anything.
*/

li + li {
  /* specificity = 0:0:2 */
}

Let’s apply these selectors to the HTML code above:

h1 + p {
  font-size: 20px;
  font-style: italic;
}

img + p {
  font-weight: bold;
  text-align: center;
}

/*
  Remove the yellow background from all list
  items, except the first one.
*/
li + li {
background-color: transparent;
}

/*
  Select all list items and set the
  background to yellow.

  I'm putting this rule here to demonstrate
  selector specificity.
*/
li {
  background-color: yellow;
}

Results in the following output:

A Header

A paragraph.

Another paragraph

  1. Item 1
  2. Item 2
  3. Item 3

Maybe a caption?

Subsequent Sibling Combinator

This is also known as the general sibling combinator and following sibling combinator4.

It is similar to the next sibling combinator in specifying the relationship between child elements, except that:

  1. the selected element does not need to be adjacent to the first element
  2. it selects ALL subsequent elements that match

The combinator is the tilde (~) symbol.

Consider:

/*
  Select all <p> elements following an <h1>
  element as long as the <p> elements are
  all siblings with the <h1> element.
*/
h1.important ~ p {
  /* specificity = 0:1:2 */
  background-color: yellow;
}

When applied to the following code:

<p>This is a paragraph.</p>
<p>So is this.</p>
<h1 class="important">This is important!</h1>
<p>This paragraph is selected.</p>
<p>So is this one.</p>
<div>
  <p>These paragraphs are not selected.</p>
  <p>Because they are not siblings.</p>
  <p>They are nested inside a div.</p>
</div>
<p>But this paragraph is selected.</p>

Results in the following:

This is a paragraph.

So is this.

This is important!

This paragraph is selected.

So is this one.

These paragraphs are not selected.

Because they are not siblings.

They are nested inside a div.

But this paragraph is selected.

Closing Thoughts

  1. The four types of combinators allow you to combine various selectors to create selectors based on their relationships. This allows for more powerful selection.
  2. The child combinator allows you to precisely specify the parent-child relationship of the elements:
parent > child > grandchild {

}
  1. The descendant combinator allows you to target elements based on whether they are nested inside other elements without caring about the specific details of the nesting:
outer-element inner-element {

}
  1. The next sibling combinator allows you to select an element if it immediately follows another element:
first-element + adjacent-element {

}
  1. The subsequent sibling combinator allows you to select all elements that follow the first element – as long as they are siblings:
key-element ~ following-elements {

}
  1. The combinators can be combined and you can more use more specific combinations of html elements, IDs, classes, and attributes:
div#main > p.important + [reversed] {

}
  1. There is no combinator that allows you to select preceding elements:
/*
  There is no select parent combinator.

  For example, you cannot style the parent
  based on the child tag.
*/
div < p {
  /* This does NOT exist */
}

/*
  There is NO preceding adjacent sibling
  combinator.

  For example, you could not select an <h1>
  element that is followed by a <p> element
*/
h1 <+ p {
  /* This does NOT exist */
}

/*
  There is no preceding sibling combinator.

  For example, you cannot select the <p>
  elements that occur before an <h1> element.
*/
h1 <~ p {
  /* This does NOT exist */
}

/*
  There is no select antedent combinator.

  For example, you cannot style an enclosing
  element based on an enclosed element.
*/
#main << .some-deeply-embedded-element {
  /* This does NOT exist */
}
  1. While it may seem unfair to not allow selecting preceding elements or parents by their children or enclosing elements from interior elements, recall that the page is rendered from top to bottom. Rendering preceding elements would require rendering the page from bottom to top or from innermost elements to outermost elements and would make application of CSS rules far more complex regarding whether top-down, bottom-up, or inner-outer rules take precedence. As well, processing from the inner elements to the outer elements is computationally more expensive – making page renders slower.
  2. You don’t need to use all combinators all the time. In fact, the descendant combinator is the most commonly used one. An analysis of 4632 CSS rules5 from 5 different websites6 revealed the following combinator distribution:7
    • 78.5% use the descendant combinator
    • 2.9% use the child combinator
    • 0.2% use the next sibling combinator
    • 0% use the subsequent sibling combinator
    • 18.4% are single item selector rules8
  3. To see some real world selector examples using combinators, consider the following from the WordPress 2019 Theme:9
/*
  Styles an element with the class .comments-title
  that immediately follows a sibling element with
  an ID of #respond that is located at any level
  inside a block with a class of .comment-form-flex.

  specificity = 1:2:0
*/
.comment-form-flex #respond + .comments-title {
  display: block;
}

/*
  Styles all elements that are direct children of
  a block with the class .comments-area.

  You've seen calc() in the lesson
  "CSS – How and When to Use Float".

  'rem' will be covered in a future tutorial,
  but it is a measurement unit like px or %.

  specificity = 0:1:0
*/
.comments-area > * {
  margin-top: calc(2 * 1rem);
  margin-bottom: calc(2 * 1rem);
}

/*
  Styles any <img> nested within the three classes
  laid out below.

  specificity = 0:3:1
*/
.entry .post-thumbnail .post-thumbnail-inner img {
  position: relative;
  display: block;
  width: 100%;
}

References

  1. Descendant Combinator
  2. Child Combinator
  3. Next Sibling Combinator
  4. Subsequent Sibling Combinator

  1. It is sometimes called closing angle bracket or right pointing angle bracket – although these are distinct symbols contrast ‘>’ with ‘〉’
  2. The space character only acts as a descendant combinator when it is alone. If you write a child combinator with spaces (.parent > .child), it is a child combinator, not a child + descendant combinator.
  3. In earlier versions of the CSS specification, it was called following sibling combinator, but the internal anchor was (and still is) general-sibling combinators (look at the end of the URL).
  4. Yes, it is a lot of rules.
  5. I pulled the CSS from the main landing pages of the following:
    • BBC,
    • Engadget,
    • The Verge,
    • Wikipedia,
    • WordPress

  6. Take these numbers as a rough guide to their relative use – if you find yourself using a lot of subsequent sibling combinators, then you are, probably, doing something wrong.
  7. This is not entirely accurate. There are other selector types you can use (which will be covered over the next few tutorials) but, for now, it is accurate enough. One such selector is the ::after pseudo-element you saw in one of the float tutorials.
  8. There is plenty in the CSS Styling that you haven’t seen – but they will be covered in future tutorials.