DEV Community

Cover image for CSS 101: Structural pseudo-classes
Damien Cosset
Damien Cosset

Posted on • Originally published at damiencosset.com

CSS 101: Structural pseudo-classes

Introduction

CSS allows you to apply styles to elements when their state changes, when the documents changes or when certains patterns are found in the document. We call them pseudo-classes. They are always preceded by a colon (:). They always refer to the element they are attached to, not their descendants. This is an important point to remember moving forward.

Most of the pseudo-classes are structural. They work with the structure of your markup. I want to target the third paragraph, the first link, the last list item ...

In this article, we will explore:

  • :empty
  • :only-child
  • :only-of-type
  • :first-child and :last-child
  • :first-of-type and :last-of-type
  • :nth-child() and :nth-last-child()
  • :nth-of-type() and :nth-last-of-type()

Got some markup?

The :empty pseudo-class targets empty nodes. This means text AND whitespace.

<p></p> => This is empty
<p> </p> NOT EMPTY
<p> 
</p>  NOT EMPTY
<p><!-- comment html --></p> Empty!
Enter fullscreen mode Exit fullscreen mode

Careful! This selector also matches elements like img and input.

:empty {
    display: none;
    /* hides all your inputs, img and other empty nodes!!!*/
}
Enter fullscreen mode Exit fullscreen mode

No siblings here

The :only-child pseudo-class targets elements if they are the only child of their descendants.

This: p span:only-child{} will target span elements if they are the only child of a p descendant.

<p>
    <strong>
        <span>I am the only child of strong which is a descendant of p ==> MATCH
        </span>
    </strong>
</p>
Enter fullscreen mode Exit fullscreen mode

In our example, span doesn't have to be the direct child of the paragraph. If the span is a descendant of the paragraph, it will be targeted too. It just has to be the only child of another element. If you wanted to make sure the span was the direct only child of the paragraph, you would have to use the > child combinator:

p > span:only-child{} would not target the span in our markup above.

I have siblings, but not like me.

Let's consider a different markup:

<p class="content">
    I am a <strong>dangerous</strong> individual when it comes to <em>CSS</em>. I can break <span>your design</span> in no time.
    <section>
        But I try my <span>best</span> to be responsible with <span>CSS</span>.
    </section>
</p>
Enter fullscreen mode Exit fullscreen mode

There are a few span elements in there. I want to target the span only if it is the only span among its siblings. I can achieve this with :only-of-type

.content span:only-of-type{} will target any span element inside any element with the content class if it is the only span among its siblings.

This would target my span in the paragraph, but not in the section, because there are two of them.

First and last children

New markup:

<section>
    <h1>First!</h1>
    <p>
        <ul>
            <li>Hello</li>
            <li>List here</li>
        </ul>
    </p>
    <h2>Second!</h2>
</section>
Enter fullscreen mode Exit fullscreen mode

I want to target the first h1 and the first li. With :first-child, I can use the following CSS:

h1:first-child {}
li:first-child {}
Enter fullscreen mode Exit fullscreen mode

A common mistake is to believe that this will select the first child of an h1 element, or the first child of a li element. Remember that pseudo-classes ALWAYS acts on the element they are associated with.

Here we target h1 and li elements when they are the first child of any element. In our example, the parents types are irrelevant. But we can select list items elements when they are the first child of a unordered list element:

ul li:first-child{}

The :last-child pseudo-class is the opposite of :first-child and targets the ...wait for it... last child!!

p span:last-child{} would select span elements when they are the last child of a paragraph.

Note: If you combine :last-child and :first-child, you get the same result than :only-child

p span:first-child:last-child{} === p span:only-child{}

Same, but with types?

The pseudo classes first-of-type and last-of-type works the same way, but with the elements types.

Markup please:

<body>
    <h1>Selected: first inside body</h1>
    <section>
        <h1>Selected: first inside section</h1>
        <h1>Not selected</h1>
    </section>
    <h1>Not selected, second inside body</h1>
</body>

Enter fullscreen mode Exit fullscreen mode

This rule h1:first-of-type{} would select the first h1 element inside another element. It targets the first type it finds inside another element. In our case, inside every element because we didn't specify a ancestor in the rule. We forget any other siblings with the same type.

<body>
    <section>
        This is not targeted
        <section>This is targeted! Last child inside section.</section>
    </section>
    <section>I am targeted!!</section>
</body>
Enter fullscreen mode Exit fullscreen mode

section:last-of-type{} will target the last section element inside another element.

With :last-of-type and :first-of-type, you are targeting an element among its siblings. You look for an element's children, check they types, and choose the first ( or last ). You can of course be more specific than the two rules I wrote above:

body section:last-of-type{}
section h1:first-of-type{}
Enter fullscreen mode Exit fullscreen mode

Note: Just like before, you can combine last-of-type and :first-of-type to get the same result than :only-of-type.

Every nth children

Targeting first or last stuff is cool, but limited. What about the second, the fourth, the eighth ...

With :nth-child(), we can target whatever child we want. This pseudo-class takes an integer ( 1, 2, 56 ) or a simple algebraic calculation.

So this li:nth-child(1){} is equivalent to li:first-child{}

p:nth-child(2) would target the second child of any element as long as the child is a paragraph.

.main h2:nth-child(4) would target the fourth child inside elements with class main as long as this child in an h2 element.

You can enhance the power of nth-child by using a simple operation. It takes the form an + b. a and b are integers. The part (+ b) is optional and defines the starting point. You would use it like so :

li:nth-child(4n + 1){}

This would select every fourth list items (4n) starting from the first one ( + 1 ). There are no zeroth index, it starts at 1. So it would target the items n° 1, 5, 9, 13 ....

If I omitted the second part ( + 1 ), I would have targeted the items n° 4, 8, 12, 16 ...

To select the odd-numbered and even-numbered children, you can use the keywords even and odd.

tr:nth-child(odd){} === tr:nth-child(2n + 1)
tr:nth-child(even){} === tr:nth-child(2n)

You can also use :nth-last-child() which works the same way than :nth-child() but it starts from the last element in the list of siblings and count backwards.

If you want to make sure that the last item is always targeted, you could do: li:nth-last-child(odd){}. This targets the items n° 1, 3, 5, 7 starting from the last one.

You can also use negative number in the optional part of those two pseudo classes:

tr:nth-child(2n - 1)

nth-of-type

In a similar manner, you have :nth-of-type and :nth-last-of-type. The concept is exactly the same, except that you are working only with elements types.

.danger p:nth-of-type(2){} => will target the second paragraph descending from an element with the class danger.

span:nth-of-type(3n + 2){} => will target every third span starting from the second span found, descending from any element.

Conclusion

Structural pseudo-classes allows you to take full advantage of the structure of your markup. You don't always have to abuse classes to make sure your markup is targeted like you want to. Smartly using pseudo-classes could be a lot better.

Top comments (0)