A Field Guide to CSS-in-JS

Scott Taylor
9 min readJan 9, 2018

--

Emotion is emerging as the leading CSS-in-JS framework out there

You probably already have whiplash from how fast the Node/React ecosystem moves and how quickly new ideas and frameworks emerge. That being said, CSS-in-JS is worth taking a look at, and I will show you why.

Ecosystem

Emotion
Emotion has a lot of momentum around it, and a new website. IMO, it has the best features of any of the projects out there, and its authors are laser-focused on performance.

Styled Components
styled-components is an OG, also a great project. It lacks some SSR features — does not currently support streaming the Node response. We can find inspiration in some of its associated packages and use others that are framework-agnostic.

How to Install

styled-theming is written by James Kyle (Yarn, Babel, Flow, Lerna) and is part of the styled-components family (also check out polished). The rest of these packages are from the emotion-js clan:


yarn add emotion emotion-server emotion-theming react-emotion facepaint styled-theming

We need the Babel plugin:

yarn add --dev babel-plugin-emotion

In .babelrc:

{
"plugins": ["emotion"]
}

In Atom, install the language-babel package to get syntax highlighting.

Syntax

Emotion and Styled Components are great because they support the 2 prevailing syntaxes in the CSS-in-JS universe:

  • Tagged-template literal (TTL): write actual CSS!
  • JS object: styles a la Yoga/React Native
import { css } from 'emotion';const buttonClass = css`
margin: 20px 0;
`;
const buttonClass = css({
margin: '20px 0',
});

There is no rule as to when to use which syntax. I would err on the side of using TTLs over JS, but there are rare times where JS makes more sense.

If you do not need to perform logic in your styles, try to just use css and place class names on HTML elements.

CSS classes are what we have been generating when we use SCSS/CSS Modules, but do we actually care what the class is named most of the time? What we care most about is encapsulating our styles and not falling prey to the dangers of the cascade. We really want to be able to associate styles with a component, so it is awesome when the styles ARE the component:

import styled from 'react-emotion';const Button = styled.button`
margin: 20px 0;
`;
export const () => <Button>Press me!</Button>

Any props you pass to the <Button> will get forwarded to the underlying DOM node <button>, so you don’t lose anything you are used to from just rendering HTML elements.

One thing to note: if you want to set a ref callback to access the actual DOM node, you need to use innerRef on a styled component:

import React, { Component } from 'react';
import styled from 'react-emotion';
const Button = styled.button`
margin: 20px 0;
`;
// lame example
export default class RadButton extends Component {
onClick = e => {
e.preventDefault();
this.button.blur();
};
render() {
return (
<Button
onClick={this.onClick}
innerRef={ref => {
this.button = ref;
}}
>
Press me!
</Button>
);
}
}

Anything that accepts a className prop can be passed to the function syntax version of styled():

import styled from 'react-emotion';
import Button from 'components/Button';
const PinkButton = styled(Button)`
background-color: hotpink;
`;

Interpolation

Most of the time, you do not want to pass arbitrary values into your styled components for things like colors, breakpoints, font-weight/family, and the like. You can pass variables, objects, and functions into your TTLs via interpolation:

import styled from 'react-emotion';
import Button from 'components/Button';
import themeUtils from 'components/themeUtils';
const PinkButton = styled(Button)`
background-color: ${themeUtils.color.hotpink};
`;

Inheritance/traits via css:

import styled from 'react-emotion';
import { css } from 'emotion';
import Button from 'components/Button';
import themeUtils from 'components/themeUtils';
const bigButton = css`
height: 60px;
width: 200px;
`;
const PinkButton = styled(Button)`
${bigButton};
background-color: ${themeUtils.color.hotpink};
`;

And via objects:

import styled from 'react-emotion';
import Button from 'components/Button';
import themeUtils from 'components/themeUtils';
const bigButton = {
// bad example
height: `${themeUtils.maxHeight / 10}px`,
width: `${themeUtils.maxWidth / 10}px`,
};
const PinkButton = styled(Button)`
${bigButton};
background-color: ${themeUtils.color.hotpink};
`;

You can access the props passed to your component from within styled!

import styled from 'react-emotion';
import Button from 'components/Button';
import themeUtils from 'components/themeUtils';
const PinkButton = styled(Button)`
${bigButton};
width: ${p => p.width || 200}px;
background-color: ${themeUtils.color.hotpink};
`;
<PinkButton width={300} />

A word of caution — the CSS classnames returned are non-deterministic: you cannot predict what they will be called. When you pass functions as interpolations — each possible variation of return value will change the classname that is generated.

Theme-ing

You can set theme variables for use in your components by using a ThemeProvider:

import React from 'react';
import { ThemeProvider } from 'emotion-theming';
import App from 'components/App';
export default () => (
<ThemeProvider theme={{ mode: 'dark' }}>
<App />
</ThemeProvider>
);

tl;dr ThemeProvider uses React context.

When a theme is present, you can get the theme prop within styled:

import styled from 'react-emotion';export default styled.button`
background: ${p => p.theme.mode === 'dark' ? '#fff' : '#000'};
color: ${p => p.theme.mode === 'dark' ? '#000' : '#fff'};
`;

I like to use the theme to set global flags or configuration, things like: “dark” or “light” mode, “large” or “small” size. You might be tempted to throw all of your colors, fonts, etc into the theme prop. I would put all of those values into a module called themeUtils and interpolate them directly:

import styled from 'react-emotion';
import themeUtils from 'components/themeUtils';
export default styled.button`
background: ${p => p.theme.mode === 'dark' ?
themeUtils.color.white : themeUtils.color.black};
color: ${p => p.theme.mode === 'dark' ?
themeUtils.color.black : themeUtils.color.white};
`;

styled-theming makes this a little less of a mess:

import theme from 'styled-theming';
import styled from 'react-emotion';
import themeUtils from 'components/themeUtils';
const buttonStyles = theme('mode', {
dark: {
background: themeUtils.color.white,
color: themeUtils.color.black,
},
light: {
background: themeUtils.color.black,
color: themeUtils.color.white,
},
});
export default styled.button`
${buttonStyles};
`;

styled-theming works anywhere a callback that is passed props with themeworks — hence, when we use ThemeProvider in our tree. You do not have to return multiple properties, you can use the same method to return just one value:

import theme from 'styled-theming';
import styled from 'react-emotion';
import themeUtils from 'components/themeUtils';
const background = theme('mode', {
dark: themeUtils.color.white,
light: themeUtils.color.black,
});
export default style.button`
background: ${background};
`;

styled-theming is a cleaner approach to interpolating logic. Without it, you might have to do this, which gets gross:

export default style.button`
background: ${p => {
if (p.theme.mode === 'dark') {
return themeUtils.color.white;
} else {
return themeUtils.color.black;
}
}};
`;

Speaking of ThemeProvider, you can extend the theme in specific portions of your app. Say you have the following structure:

<App>
<Header />
<Content />
<Footer />
</App>

You can theme it like so (pseudo representation):

import { ThemeProvider } from 'emotion-theming';<ThemeProvider theme={{ town: 'flavor' }}>
<App>
<ThemeProvider theme={theme => ({ ...theme, sauce: 'donkey' })}>
<Header />
</ThemeProvider>
<Content />
<Footer />
</App>
</ThemeProvider>

Class names

There are times when you want to add multiple classes to a component conditionally, and the classnames package is useful for that. We get a high-performance package as part of emotionas an export called cx:

import { cx } from 'emotion';
import Button from 'components/Button';
export default () => (
<Button
className={cx('flavortown', {
donkeySauce: truthyValue,
bluesabi: falseyValue,
})}
>
Whoa
</Button>
);

Just because Emotion generates indecipherable classnames does not mean that you cannot use deterministic classnames as well, particularly when using classes to represent state like: open, closed, active, etc. Sometimes it is easier just to do:

import styled from 'react-emotion';const Menu = styled.nav`
height: 36px;
&.open {
height: 128px;
}
`;

You can accomplish the same thing by passing props to your styled component and then reading the props to set the value. There will be times that deterministic classnames are easier to understand — trust me, don’t be a hero here.

Media Queries

Media queries work just like you expect:

import styled from 'react-emotion';const Menu = styled.nav`
height: 36px;

@media (min-width: 320px) {
height: 40px;
}

@media (min-width: 600px) {
height: 54px;
}

@media (min-width: 1024px) {
height: 120px;
}

@media print {
height: 20px;
}
`;

This is more concise that straight CSS, but also annoyingly long. Emotion has a supplemental package called facepaintthat can help us with this.

import styled from 'react-emotion';
import facepaint from 'facepaint';
import themeUtils from 'components/themeUtils';
const mq = facepaint([
themeUtils.breakpoint.small,
themeUtils.breakpoint.medium,
themeUtils.breakpoint.large,
'@media print'
]);
// first value is default
const responsive = mq({
height: ['36px', '40px', '54px', '120px', '20px'],
// null and undefined values are skipped
width: ['100px', null, null, '300px'],
// no responsive values are printed for this one
display: 'block',
});
const Menu = styled.nav`
${responsive};
`;

Combined with styled-theming, we can easily generate themed responsive styles:

import styled from 'react-emotion';
import facepaint from 'facepaint';
import theme from 'styled-theming';
import themeUtils from 'components/themeUtils';
const mq = facepaint([
themeUtils.breakpoint.small,
themeUtils.breakpoint.medium,
themeUtils.breakpoint.large,
'@media print'
]);
const responsive = theme('mode', {
dark: mq({
height: ['36px', '40px', '54px', '120px', '20px'],
width: ['100px', null, null, '300px'],
background: themeUtils.black,
}),
light: mq({
height: ['36px', '40px', '54px', '120px', '20px'],
width: ['100px', null, null, '300px'],
background: themeUtils.white,
})
});
const Menu = styled.nav`
${responsive};
`;

Global Styles

As you can already tell, coupling styles with components eliminates an entire class of precedence concerns. The styles associated with a component are exactly the styles you declare with it. There are times though, where you want to declare global styles for a project.

There are 2 ways to do this:

  • Declaring your global styles (reset, normalize, etc) in a separate file.
  • This escape hatch:
import { injectGlobal } from 'emotion';
injectGlobal(`
body {
/* rules go here */
}
a {
color: #000;
}
`);

I suggest using a file. injectGlobal works best when it is called once on the server (at the right moment) and is not called on the client.

SSR

We definitely want to load critical styles on the server. We will only be loading the styles we are actually using at render-time. This is amazing. As your codebase grows, you can end up with a metric ton of SCSS. Many Webpack build methods load ALL of this CSS at once, not discriminating at all. CSS-in-JS with extractCritical() in Emotion solves this problem.

Critical CSS

This is oversimplified, but here is the flow for extracting styles on the server, outputting them, and then telling Emotion what it has already loaded when hydrating on the client.

import React from 'react';
import { renderToString } from 'react-dom/server';
import { extractCritical } from 'emotion-server';
import App from 'components/App';
export default (req, res) => {
const { html, css, ids } = extractCritical(renderToString(<App />));
res.send(`<!DOCTYPE html>
<html>
<head>
${css ? `<style>${css}</style>` : ''}
<script>window.__emotion = ${JSON.stringify(ids)}</script>
</head>
<body>
<div id="app">${html}</div>
</body>
</html>
`);
};

Rehydrate on the client

import React from 'react';
import ReactDOM from 'react-dom';
import { hydrate } from 'emotion';
import App from 'components/App';
hydrate(window.__emotion);ReactDOM.hydrate(
<App />,
document.getElementById('app')
);

In the flow of your app (Single Page App or app with a dynamic UI), when Emotion encounters styles it has not previously loaded, it will insert them into the <head> dynamically when rendering components. It caches heavily, so no unnecessary compilation occurs for work that is already done.

Streaming

Emotion also supports loading critical CSS when streaming the response. Emotion will insert the rules inline right before they are needed. This results in many <style> tags in the server response, instead of one big chunk that you include at the top.

import React from 'react';
import { renderToNodeStream } from 'react-dom/server';
import { renderStylesToNodeStream } from 'emotion-server';
import App from 'components/App';
export default (req, res) => {
renderToNodeStream(app)
.pipe(renderStylesToNodeStream())
.pipe(res);
};

A caveat here: Emotion will automatically hydrate on the client. It does this by ripping out all of the styles from the document and moving them to the <head>. I got this working without a flash of unstyled content, but there are weird bugs that can arise if any of your CSS rules deal with pseudo-selectors like :nth-child(), :first-child(), etc. The <style> tags count as children and can mess up rules like :nth-child(odd). :nth-of-type(odd) is a quick fix.

Whoa

Your eyes may be bleeding and your mind racing. I just wanted to give an overview of what is out there, and share a few things I have learned in my research. As we continue to transition the New York Times to our modern codebase written in React and Apollo, we are also looking at ways to make theming and responsive styles easier. We are also trying to make our components as atomic and shareable as possible. Stay tuned as we grow and learn.

--

--

Scott Taylor

Musician. Staff Engineer at Shopify. Formerly: Lead Software Engineer at the New York Times. Married to Allie.