A Little Reminder That Pseudo Elements are Children, Kinda.

Avatar of Chris Coyier
Chris Coyier on (Updated on )

UGURUS offers elite coaching and mentorship for agency owners looking to grow. Start with the free Agency Accelerator today.

Here’s a container with some child elements:

<div class="container">
  <div>item</div>
  <div>item</div>
  <div>item</div>
</div>

If I do:

.container::before {
  content: "x"
}

I’m essentially doing:

<div class="container">
  [[[ ::before psuedo-element here ]]]
  <div>item</div>
  <div>item</div>
  <div>item</div>
</div>

Which will behave just like a child element mostly. One tricky thing is that no selector selects it other than the one you used to create it (or a similar selector that is literally a ::before or ::after that ends up in the same place).

To illustrate, say I set up that container to be a 2×3 grid and make each item a kind of pillbox design:

.container {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-gap: 0.5rem;
}

.container > * {
  background: darkgray;
  border-radius: 4px;
  padding: 0.5rem;
}

Without the pseudo-element, that would be like this:

Six items in a clean two-by-two grid

If I add that pseudo-element selector as above, I’d get this:

Six items in a two-by-two grid, but with a seventh item at the beginning, pushing elements over by one

It makes sense, but it can also come as a surprise. Pseudo-elements are often decorative (they should pretty much only be decorative), so having it participate in a content grid just feels weird.

Notice that the .container > * selector didn’t pick it up and make it darkgray because you can’t select a pseudo-element that way. That’s another minor gotcha.

In my day-to-day, I find pseudo-elements are typically absolutely-positioned to do something decorative — so, if you had:

.container::before {
  content: "";
  position: absolute;
  /* Do something decorative */
}

…you probably wouldn’t even notice. Technically, the pseudo-element is still a child, so it’s still in there doing its thing, but isn’t participating in the grid. This isn’t unique to CSS Grid either. For instance, you’ll find by using flexbox that your pseudo-element becomes a flex item. You’re free to float your pseudo-element or do any other sort of layout with it as well.

DevTools makes it fairly clear that it is in the DOM like a child element:

DevTools with a ::before element selected

There are a couple more gotchas!

One is :nth-child(). You’d think that if pseduo-elements are actually children, they would effect :nth-child() calculations, but they don’t. That means doing something like this:

.container > :nth-child(2) {
  background: red;
}

…is going to select the same element whether or not there is a ::before pseudo-element or not. The same is true for ::after and :nth-last-child and friends. That’s why I put “kinda” in the title. If pseudo-elements were exactly like child elements, they would affect these selectors.

Another gotcha is that you can’t select a pseudo-element in JavaScript like you could a regular child element. document.querySelector(".container::before"); is going to return null. If the reason you are trying to get your hands on the pseudo-element in JavaScript is to see its styles, you can do that with a little CSSOM magic:

const styles = window.getComputedStyle(
  document.querySelector('.container'),
  '::before'
);
console.log(styles.content); // "x"
console.log(styles.color); // rgb(255, 0, 0)
console.log(styles.getPropertyValue('color'); // rgb(255, 0, 0)

Have you run into any gotchas with pseudo-elements?