How to add vanilla Emotion server-side rendering to Gatsby

• 3 min read

On the whole, having used Emotion on a few of my last projects, I'm quite happy with it compared to other CSS-in-JS libraries. I like that they offer multiple APIs, allowing you flexibility in how you want to style your components. Personally, I prefer using the plain css API as the others seem to have major drawbacks (one for another post).

Emotion pushes you to style elements your elements with their css prop, instead of className. There are some advantages from doing this - you get out of the box server-side rendering (SSR) and theming. Unfortunately, there's also some big disadvantages, in particular the super awkward ClassNames component. Colin McDonnell explores a few of the issues in much greater detail in his post Why you shouldn't use @emotion/core.

Thankfully, you can avoid the css prop entirely by using the vanilla @emotion/css package. The main caveat here is that you have to manually setup server-side rendering. This can be a little tricky with Gatsby as it's not obvious how to do this from the documentation. If you look closely, none of it actually describes how to integrate with vanilla @emotion/css. What is the correct way?

Add a CacheProvider

Firstly, we need to add CacheProvider components to both the server and client to allow us to integrate correctly with React. We can do this by adding the following to both gatsby-ssr.js and gatsby-browser.js:

import { cache } from '@emotion/css'; // This is really important!
import { CacheProvider } from '@emotion/react';

export const wrapRootElement = ({ element }) => {
  return <CacheProvider value={cache}>{element}</CacheProvider>;
};

The key part here is that we're using the cache exported from @emotion/css as this is the cache instance that our css calls use. We're using Gatsby's wrapRootElement API to allow the Emotion cache to wrap the entire application. This is similar to how you might add any other standard React context provider.

Inject server-side rendered styles

Finally, we just need to inject any styles that Emotion generates into the server-side rendered markup. Gatsby exposes a few APIs that can be, however, I've found that the replaceRenderer API was the most concise way of doing this. Be aware that it may have its own caveats depending on if you're using plugins that rely on it too.

In gatsby-ssr.js, we can add something like the following:

import createEmotionServer from '@emotion/server/create-instance';
import { renderToString } from 'react-dom/server';

const { extractCritical } = createEmotionServer(cache);

export const replaceRenderer = ({ bodyComponent, setHeadComponents }) => {
  const { css, ids } = extractCritical(renderToString(bodyComponent));

  setHeadComponents([
    <style
      key="app-styles"
      data-emotion={`css ${ids.join(' ')}`}
      dangerouslySetInnerHTML={{ __html: css }}
    />,
  ]);
};

This essentially renders our Gatsby app into a string on the server, before Emotion's extractCritical is used to pull out all of the various classes (in the ids variable) and styles (in the css variable). Using Gatsby's setHeadComponents, we can inject all of this into our HTML's head as a style element.

Note that in the style element's data-emotion attribute, I've used a key of css as this is the default used by @emotion/css. If you want to change it, you should check out the documentation on Custom Instances. It's pretty unlikely you'll need this besides for a specific set of use-cases though.

Finishing up

Now that our styles have been injected, we just need to check to check that everything is working correctly. Build and serve your Gatsby app:

gatsby build
gatsby serve

Finally, disable JavaScript in your browser and visit your app URL. If everything is correct, you should be able to see that everything is styled correctly. This means that the server-side rendered styles have been injected properly. Without them the app would be unstyled as Emotion cannot run client-side with JavaScript disabled.

That's it! Enjoy using vanilla Emotion with Gatsby.