Patching Lobotomized Owl selector for Emotion SSR

Photo by Quino Al on Unsplash

Recently I’ve been working on a Gatsby theme website to enable teams at commercetools to write documentation for different parts of the product. The Gatsby theme uses some of our UI-Kit Design System elements and UI components, some of which are used for layout purposes. We call them Spacings components and they all use a pretty interesting CSS selector: the Lobotomized Owl selector.

> * + * {}

A (ex)colleague of mine introduced this to me some years ago and since then I’ve been using it wherever I can. It just makes things way easier to style.

Our UI components, and the Gatsby theme, use the Emotion CSS-in-JS library for styling purposes. It’s a wonderful library to work with and we’ve been quite happy with it so far.

Undesired style effects

At some point during the development of the website, we bumped into an issue in production mode: most of the elements on the page were “moving” after the page was loaded. At first it appeared that we were experiencing the infamous FOUC (Flash Of Unstyled Content).

Upon further investigation, it was clear that the issue was caused by how Emotion deals with SSR (Server-Side Rendering) in relation to the Lobotomized Owl selector. Unfortunately, this is a known issue for Emotion, see GitHub issue about this topic.

To be able to find a viable solution, we need to understand the problem at hand first.

How Emotion SSR works

Emotion comes with support for SSR, which is necessary for tools like Gatsby to be able to generate static pages. The browser renders the static HTML markup first, then Gatsby client side JavaScript kicks in and hydrates the page which is then managed by JavaScript and React.

Server side rendering in Emotion 10 has two approaches, each with their own trade-offs. The default approach works with streaming and requires no additional configuration, but does not work with nth child or similar selectors. It’s strongly recommended that you use the default approach unless you need nth child or similar selectors.

The static page contains inline <style> elements injected by Emotion, next to each HTML element that requires those styles. When the page is loaded and hydrated, Emotion moves all those inline <style> elements to the <head> of the document. This is what makes the elements “move” after the page is hydrated.

The reason is simple: elements that implement the Lobotomized Owl selector might have undesired <style> elements as children, because of Emotion SSR. This disrupts the CSS selector to count a wrong element in the adjacent siblings. For example, elements applying a margin-top to each children besides the first one will possibly result in the first element having the margin as well, only to be removed once the injected <style> elements get moved away.

<div class="css-161caka e1gnsc3u4">
<style data-emotion-css"">.css-1yzfhzg{font-size:3rem}</style>
<h1 class="css-1yzfhzg">Hello World</h1>
<div>How are we doing?</div>
</div>

In the example above, the <h1> element gets the margin-top as long as the <style> element stays there.

Approaching the solution

Ideally the <style> elements injected by Emotion should not be there and interfere with the CSS selectors. This is a conscious choice by the Emotion team and therefore we need to find a different solution.

The problem with the Lobotomized Owl selector in this scenario is that it uses the adjacent sibling combinator +.

The adjacent sibling combinator (+) separates two selectors and matches the second element only if it immediately follows the first element, and both are children of the same parent element.

In other words, the Lobotomized Owl selector picks all the sibling elements except the first one, because it does not have any sibling element before that.

Excluding the style element

What we need is a CSS selector that excludes the style element from the results, thus keeping the correct styles as they were intended to.

To exclude something from a selector we can use the :not pseudo-class and apply it to the style element: :not(style).

Ideally the selector should read: select all sibling elements that follow another element, except if the element is a style element.

Possible combinations of the new selector are:

> *:not(style) + * {}
> * + *:not(style) {}
> *:not(style) + *:not(style) {}

Which one works? The answer: none of them. 😱

Each selector has a scenario where it does not work as it should (see Codesandbox below).

Understanding the general sibling combinator

How can we solve this problem? After a bit of research I came across another combinator: the general sibling combinator.

The general sibling combinator (~) separates two selectors and matches the second element only if it follows the first element (though not necessarily immediately), and both are children of the same parent element.

If we modify the Lobotomized Owl selector with this combinator and try to exclude the style element, we have:

> *:not(style) ~ * {}
> * ~ *:not(style) {}
> *:not(style) ~ *:not(style) {}

Does it work now? Yes, it does! 🙌

In the Codesandbox below you can see a comparison of the results of using both combinators with different scenarios. The two green selectors on the rigth side are the winners of our experiment, which successfully apply the styles to the elements regardless of the position of the <style> element.

Comparison of adjacent and general sibling combinator for excluding style elements.

Patching Emotion SSR in Gatsby

How can we make this work in our Gatsby website? One approach is to directly patch the CSS selectors but it’s kind of error prone and not ideal.

A less nicer but effective approach is to patch the generated static HTML and CSS. To do so, we need to follow the advanced approach described in the Emotion docs. Then we can patch the Lobotomized Owl selectors by doing a search-replace on the HTML and CSS strings, before passing them to Gatsby.

Patching the HTML and CSS in gatsby-ssr.js.

Conclusion

While this solution seems to work, I wish there is a more robust approach to deal with this issue. Maybe Emotion v11 will reserve some positive surprises.

Software Engineer @commercetools, Dad, Technology Enthusiast. I ❤️ building things.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store