DEV Community

Cover image for APB: A Real Look at Pseudo-Selectors
Ash
Ash

Posted on

APB: A Real Look at Pseudo-Selectors

When I began seriously learning CSS - mostly by pouring through the repositories of my betters and experimenting with what I found there - I was mystified by pseudo-selectors, and especially pseudo-elements. Intimidated, I quickly jotted down a new question for the All Points Bulletin Board and moved along:

What are pseudo-elements? What do ::before and ::after refer to?

When I returned and set out to answer these questions, I found I couldn't learn about pseudo-elements separate from pseudo-classes. The two are inherently linked, working on an odd plane of specific un-specificity, and the real magic becomes apparent when the two are combined.

We'll first take a look at pseudo-classes. Though pseudo-classes are broader, and there are many more of them, I find them easier to reason about.


Pseudo-Classes

A pseudo-class is a CSS simple selector that targets elements that exist in a specific state. That state can be whether or not they're the first child of their parent, if they're actively selected or hovered and so on.

Pseudo-classes act like any another class in CSS - allowing us to write specific rules for parts of the document, with the added benefit of keeping our html markup free and clear of excess class names.

The basic syntax looks like a keyword glued to the end of a selector with a single colon:

element:pseudo-class {
    property: value;
}
Enter fullscreen mode Exit fullscreen mode

👉 Note: It's valid to use a pseudo-class without the element selector preceding it - but be wary. Part of the magic of pseudo-classes is how their specificity works. By nature, their specificity weight is low, stripping the element selector makes them even less specific - compiling to *:pseudo-class.

There are several pseudo-classes you're probably already familiar with that often come in handy:

  • :first-child
  • :invalid
  • :last-child
  • :active

And you're sure to have seen this common use case a time or two:

button:hover {
    color: red;
}
Enter fullscreen mode Exit fullscreen mode

This example references the button elements, then uses the pseudo-class :hover to apply a rule. In this case, when a button on the page is in a hovered state then the pseudo-class (and any rule it contains) applies, making the font color red.

It should be noted that :hover is in a group of special pseudo-classes, known as user-action or dynamic pseudo-classes. These perform a little differently, giving or taking away a pseudo-class depending on user interaction, like hovering. Other examples of dynamic pseudo-classes include :focus and :active.

Using pseudo-classes is something like getting class names for free, available if and when we need them. Meaning, we can specifically style pieces of text, colors, font-sizes, box-shadows and more without cluttering the html markup with extraneous classes attached to elements.


🙋‍♂️ To conceptualize what all that means, let's imagine we have a client that has hired us to build a basic website to share their favorite pieces of writing. We built the following page, using html and CSS:


The client, while happy with the overall design, has requested that the first paragraph (the first <p> element) of every post be in a bolder typeface.

Your first instinct might be to add a class to the html. Perhaps one conveniently named - like "first".

<article class="post-body">
  <h2>Lorem Title</h2>
  <small>by Alexander Wagner 2020</small>
  <p class="first">Lorem ipsum dolor sit amet...</p>
  <p>Aenean pharetra convallis pellentesque...</p>
  <div>
    <button>Comment</button>
    <button>Save</button>
  </div>
</article>
Enter fullscreen mode Exit fullscreen mode

This will work, but there's a few problems with the approach. First, we have to go through and add the class to every first paragraph. And what if the first paragraph were to change at some point? Adding another class to achieve this result is prone to typos, is hard to maintain and update, and isn't very DRY.

Instead we'll use the :first-of-type pseudo-class - a class we get for free, that maintains itself. And to use it we won't have to change a thing in the html.

article p:first-of-type {
    font-size: 120%;
    font-weight: bold;
}
Enter fullscreen mode Exit fullscreen mode

In plain English article p:first-of-type translates loosely to "First, look for every article element in the document. Then search their children for the first occurrence of a paragraph element. Finally, apply these rules to it." The :first-of-type pseudo-class is in yet another sub-classification of pseudo-classes, known as structural-tree pseudo-classes.

Pseudo-classes are broad - there's a lot of them, each one doing something a little different than the last. But in general, pseudo-classes allow us to dynamically style a page - reacting to screen interactions and providing visual reassurance to users - all without changing and cluttering the html.


Pseudo-Elements

A pseudo-element can be used to style specific parts of an element - like the first line or first letter. They can also be used to add a whole new element to the document, allowing us to insert content before, or after, the contents of an element. With this approach, for every one element we build in html - we get two for free!

Like pseudo-classes, their syntax is comprised of a keyword (the reserved pseudo-element name) added to a CSS selector - but this time with a double colon:

selector::pseudo-element {
    property: value;
}
Enter fullscreen mode Exit fullscreen mode

👉 Note: The extra colon isn't strictly required (for CSS2 and CSS1) for some pseudo-elements, but is added by convention to easily distinguish between pseudo-elements and pseudo-classes. Because some early pseudo-elements used only a single colon, modern browsers support backwards compatibility for this usage.

There are seven pseudo-elements that are considered non-experimental. Five of them work to target different parts of an existing element:

  • ::first-letter
  • ::first-line
  • ::marker
  • ::placeholder
  • ::selection

The remaining two work differently than the rest, creating entirely new elements when they're used:

  • ::after
  • ::before

👉 Some pseudo-elements have varied support across browsers, like ::marker and ::selection. Stop over to Can I Use? to learn more.


🙋‍♂️ The client has returned, armed with ideas to refine their website. They've requested a stylish icon appear before the title of each article. As with many tasks in programming, there's more than one way for us to do this.

We could use a <span> element to wrap a special html character before each title, styling it independently as needed.

<div class="post-body">
  <h2><span>&#9815;</span>Lorem Title</h2> 
/* 👆 This special character "&#9815;" is a rook symbol */
  <small>by Alexander Wagner 2020</small>
  <p>Lorem ipsum dolor sit amet...</p>
  <p>Aenean pharetra convallis pellentesque...</p>
  <div>
    <button>Comment</button>
    <button>Save</button>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

You already know the problems with this - another element will have to be added to the html for every post, creating clutter, opening up possibilities for typos, and making the page harder to maintain. Furthermore, we're not adding this element to benefit the structure of the source code - it's only being used to provide a stylistic touch - making it a perfect use case for the ::before pseudo-element.

article h2::before {
  content: "♗";
}
Enter fullscreen mode Exit fullscreen mode


The ::before and ::after pseudo-elements are special - rather than targeting a part of an element, they insert content into the document before or after the content that already exists there, using CSS. Both can only be used once per targeted element, and must be used with the content property.

Using the content property we can add:

  • A string - a plain sentence, special html symbols, etc.
  • An image - we can provide a url() or a gradient background (which is an image itself)
  • A counter - lesser used, but can be useful for a CSS developer. This counter generally represents a number produced by various computations.
  • Nothing at all - This still requires setting the content property to the value of an empty string. Usually used to insert images as background-images, or the clearfix hack.

The new elements created by using ::before or ::after are inline by default, but can be moved and positioned relative to their parent. In that case it's important to note their source-order. Thankfully, it's easy to remember - ::after content is also after in stacking order. Meaning that if it happens to overlap a ::before pseudo-element, it will cover that element.


♿ A Note on Accessibility

Content that is generated by CSS, like the things we inject with ::before and ::after, are not in the Document Object Model, commonly known as the DOM. Elements that are not in the DOM tree are also absent in the Accessibility tree. For that reason, we should only use pseudo-elements to change the look of a page, not to convey anything of importance about the page's purpose.


With Our Powers Combined!

Used independently, pseudo-classes and pseudo-elements are capable of doing some amazing things. But using them together, in unison with CSS selectors and classes we define ourselves, can open up a whole new worlds of possibility. We can chain pseudo-elements onto pseudo-classes, allowing us to be just specific enough to target parts of an element, in certain states, and apply refined rules.

🙋‍♂️ Our client sent us a late night email - they no longer want the whole first paragraph bold, instead they want to target just the first line of the post. Without pseudo-selectors this is harder to solve for.

Perhaps we could wrap the first sentence of each paragraph in a <span> element and style it. But what if that first sentence runs over the first line, or doesn't quite reach the end of it? On different screen sizes, at different font sizes, adjusting for this approach would be a complete nightmare - and arguably impossible.

But with pseudo-selectors we can achieve this by chaining the ::first-line pseudo-element on the :first-of-type pseudo-class we were already using - creating what is known as a compound selector.

article p:first-of-type::first-line {
  font-size: 120%;
  font-weight: bold;
}
Enter fullscreen mode Exit fullscreen mode


Looking at the selector, we can follow it like a word map: "Start by looking for the all <article> elements in the document, and step into them. Once there (in all of them simultaneously) look for the element that is the first of <p> type, and grab the first line - however long or short that may be, regardless of font size or screen width - and make it a little bigger and bold."

👉 The ::first-line pseudo-element applies only to block-level elements.

Given the sheer number of available pseudo-classes, there's a nearly endless amount of ways to combine them with compound selectors - and the rules aren't terribly well defined.

Generally, the order of simple selectors (aka pseudo-classes) don't change the meaning of a compound selector, and we can use as many of them as we want. But each compound selector should have only one pseudo-element - and it should appear at the end.


When we want to provide a website with more style, or unique ways for the user to interact with it, we can look to pseudo-classes and pseudo-elements. They allow developers to style things that are hard to express structurally, change at different points in time or don't exist in the document tree at all.

Using them has the benefit of producing clean, easy to maintain code that doesn't require special sizing rules across various devices. And because we don't have to touch the html (and therefore the document structure) to use them, semantics are preserved.

But they're not without their catches and gotcha's. Because the specificity weights of pseudo-selectors works so uniquely, unexpected bugs can come up when we're too specific, or likewise, when we're not specific enough. Another major barrier is cross-browser support. When using pseudo-selectors, especially experimental ones, be sure to do your research before committing to any one approach.


Quick Reference

  • A pseudo-class targets an element's various states. Allowing us to select and style the element based on information that lies outside of the document tree that would otherwise be impossible (or at least very difficult) to express using other simple selectors.

  • A pseudo-element targets different parts of an element, representing additional information about an element (like what its first letter is) that isn't directly present in the document tree.

  • There are 7 non-experimental pseudo-elements, and many more pseudo-classes that are broken down even further into several sub-categories - including dynamic, structural-tree, time-dimensional and more.

  • ::before and ::after pseudo-elements are used to insert content onto the page, but not into the document tree. They need the content property to work correctly.

  • When creating compound selectors in CSS we are free to use as many pseudo-classes as we want or need, but should use only one pseudo-element, adding it only at the very end of the selector.

  • CSS-generated content is not included in the DOM, and shouldn't be used for important information that would miss users requiring a screen reader or other adaptive technology.


🔍 Check out the resources below to learn more about pseudo-elements and (the many) pseudo-classes. While I've done my best to cover the topic as a whole, these posts illuminated the intricacies, bugs, and conceptual challenges that plagued me along the path to understand how these strange, "fake" CSS entities work.

Resources:

🦄 All feedback is welcome and as always - thank you for reading!

Top comments (0)