Another perspective of some css-in-js solutions

This post is about my experiences working with a couple of different css-in-js solutions. I’ve been working for the past year or so on a project that opted for css modules. Its pretty good but a few little niggles have come up on a number of occasions that made me want to start looking at alternatives. So, I tried out emotion, and had a little poke around at glamorous and styled-components and how they work with styled-system. Here’s what I found…

My niggles

  • Theming
  • Overriding styles and catering for exceptions
  • Dynamic styles based on props
  • Ensuring semantic and logically nested mark up

Theming

CSS Modules

I’ve recently had to try and implement theming in our app using css modules. The idea is pretty straight-forward. Instead of importing the style directly, you just pass the style to the component and it uses that.

instead of…

import style from './componentStyle.css';
const Component = () => <div className={style.root}>The content</div>;

you do…

// no import here for style
const Component = (style) => <div className={style.root}>The content</div>;

Pretty simple. A few things to note here, not necessarily good or bad.

  • If you want to set defaults, you could still import styles in the component (call them baseStyles or something), and then do some kind of merging function to merge in defaults and overridden styles.
  • The responsibility of theming still lies with the styles you either import or inject. This could get out of hand if you don’t set up a logical theme structure.

When contrasting it with how something like styled-system works with emotion or styled-components ‘ThemeProvider,’ it seems less powerful somehow.

ThemeProvider

This concept can be used with most css-in-js solutions. The basic idea is that you create a theme somewhere defining all the defaults for the app (colours, spacings, font sizes etc), then provide this as a prop to a top level ‘ThemeProvider’ component. The theme provider uses the react context API to expose the theme to any descendant styled components. This is a really powerful, yet simple concept to use and once you’ve done a few components this way, it becomes really natural. Here are some of the pros of this approach.

  • no need to pass styles around or inject them, they are just available to the right components
  • no need to do convoluted imports in your components. When using css modules, in our styles, we often had to reference stuff like ‘../../../styles/breakpoints’ or ‘../../../styles/colours’ which was a pain to manage. This could be simplified with webpack aliases, but the root problem still exists – multiple files have to reference multiple other files to compose a component’s styles together.
  • when used with styled system, its easy to define theme defaults, and then define modifiable aspects of the style. (I’ll talk about this a bit later)
  • the potential for multiple themes to be applied also exists (I haven’t had a need to play with this yet though)

Overriding styles and catering for exceptions

Sometimes in our app, we had a default style for the component. For example, a form field where most of the time, there’s some default padding on the whole thing. However, sometimes, we wanted to turn off padding or add a border or something. We could create a new component, but that seems a bit crap. So with CSS modules, we resorted to having a ‘modifierStyles’ prop that could be passed in to override the base styles. Here’s an example…

const Field = ({ children, modifierStyles }) => (
    <div className={combineClass(styles, modifierStyles, x => x.root)}>
      { children }
    </div>
);

We created a utility function called combineClass that basically uses the third parameter (x => x.root) to pick out a style from both the directly imported style and the injected ‘modifierStyles’ object based on its key. It works, but its a bit clunky. For every element you want to be able to override, you need to add in the reference to combineClass. You also need to set up the modifierStyles object in the calling component with the correct keys. It quickly becomes a pain.

Styled system to the rescue

With styled components and styled system, its really easy to define defaults, then specify which bit of the style are ‘modifiable.’ Then, when the component is instantiated, overrides can be set. Here’s an example…

import styled from 'styled-components';
import { fontSize } from 'styled-system';

const paragraphDefaults = ({ theme }) => `
  font-size: ${theme.fontSizes[0]};
  // any other defaults
`;

const Paragraph = styled.p`
  ${paragraphDefaults}
  ${fontSize}
  // any other modifiable styles
`;

So, all paragraphs will have a default font size but you can also do something like this…

<Paragraph fontSize={[0, 1, 2]}>text</Paragraph>

This allows a responsive style to be defined for the font-size, where different values get applied at different breakpoints (which you also specify in the theme).

Once nice feature here is that if you don’t specify something as modifiable, it won’t be! Seems obvious but you can use this feature to handle nested modifiable bits. e.g…

const ComplexComponent = props => (
   <div id={props.id}>
     <Paragraph {...props}>text</Paragraph>
   </div>
);

So here, you can just spread out the props and pass them to the paragraph. Styled system will apply the modifiers that are specified in the component, and ignore the rest. So if the props also had modifiers for margin, border etc, but the component didn’t accept those modifiers, they wouldn’t be applied.

This feature is a neat way to create components that are flexible but within certain boundaries. For example, you might have a button that allows consumers to update the padding, but not change the background colour. Its easy to extend and its clear from the component definition what things are modifiable. This kind of thing helps a lot when trying to create component libraries that are flexible but still define boundaries.

Handling exceptions

Sometimes, you’ll need to tweak a components value once. e.g. the designer comes to you and says, ‘can we just make the padding round the component a bit bigger?’ There’s no point adding a value to the theme since this is an exception. With styled system, its easy to handle this too. Any raw values passed to a component are interpolated directly.

<Paragraph m="5rem">biiig margin</Paragraph>

Another advantage here is that we can clearly see which aspects of a components styles are being overridden. Its not hidden in a modifier styles object.

Dynamic styles based on props

While working on one component (an expanding element), we found css modules to be quite limiting. Since the styles are in a separate css file, its pretty difficult to use dynamic values in styles. The only way we could do it was by injecting the styles directly into the component using the style attribute. Converting the component to a styled component made the process a lot simpler. Here’s the expanding element…

<ExpandingContent
   maxHeight={this.state.maxHeight}
   innerRef={(el) => { this.expandingElement = el; }}
>
  body content
</ExpandingContent>

and this is how its defined…

import styled from 'emotion';
import { maxHeight } from 'styled-system';

const ExpandingContent = styled.div`
  transition: max-height .4s ease-in-out;
  overflow: hidden;
  ${maxHeight}
`;

export default ExpandingContent;

Here, we just make ‘maxHeight’ a modifiable element. The elements actual client height is derived from the local state value ‘maxHeight’ which references the ‘expandingElement’ object.

This approach keeps all the styles in one place without having to rely on 2 different ways to calculate the values.

Ensuring semantic and logically nested mark up

An ongoing issue we face is having components that target specific tags (e.g. h1, h2) and then trying to use them in different places. So we have a ‘SectionHeader’ component that outputs an H3 tag. It encapsulates a fair few styles and is re-used throughout the app.

However, what happens when we have the page header (H1), then we want to use a SectionHeader? The heading order isn’t right. You can hopefully see there are many situations where the component nesting won’t be right (H3 inside an H4 etc etc). With css modules, there’s no way round this since the output tags have nothing to do with the styles. We’d have to either put some conditional logic in to determine the tag, pass the tag in as a prop, or separate out the styles from the markup and create a load of different components with the right tags.

With styled components, we can choose which tag to output. Simple as that. So in most cases, we can have SectionHeader output an h3. However, when they are used after an h1, we can create a modified version of it…

import BaseSectionHeader from '../SectionHeader';
export default BaseSectionHeader.withComponent('h2');

Both emotion and styled components offer this feature.

Issues with styled components and styled system

While spiking out various solutions, I found the following issues…

  • vague theme variables
  • working with CSP’s

Vague theme variables

When using styled system, some of the theme values have to be flat arrays. For example, the space key in the theme could only be a flat array. I tried using objects and referencing these in components but to no avail. Using array values in components to define spacings does take some getting used to. What I’d have liked would be something like…

const space = {
  y: ['1.2rem', '2.4rem', '3.6rem'],
  x: ['1rem', '2rem', '3rem']
};

This would allow me to easily reference different x or y padding values based on the design. Having the space defined as a flat array meant that in my component, if I wanted responsive spacing, I needed to do…

(theme.js)

const space = ['0', '1rem', '1.2rem', '2rem', '2.4rem', '3rem', '3.6rem'];

(Component.js)

<Component pl={[2, 4]}>blah</Component>

Not very readable really. It basically says, for mobile I want padding-left to be the 2nd index of the space array (1.2rem). Above that, I want it to be the 4th index (2.4rem). I may have missed something but this seems like a limitation (please correct me if I’m wrong).

CSP’s and nonces

There’s a fair bit of chat online about making styled components work with corporate CSP’s. Without going into too much detail, a content security policy (CSP) basically stops any scripts or styles being used or injected into a page without having a ‘nonce’ attribute that matches the one defined in the CSP header for that page. The way styled components works is by injecting style tags into the head of the document. So, unless these tags have the nonce attribute, they get stripped out of the page.

I have to shout out here that I raised the question about CSP’s in an open styled components issue on github and within around half an hour got responses, including one from of the main repo contributors. So that’s pretty nice.

I found 2 ways round the CSP issue.

  1. Using webpack’s webpack_nonce feature, you can get webpack to automatically inject the nonce attribute into every script or style tag that the app injects.
  2. Using emotion, you can create a custom instance which includes the nonce, and then reference that in the app for creating styled components

Webpack_nonce

This method definitely seemed more straight-forward, once a few hoops have been jumped through. Here’s an outline of the general approach…

  • The server-side code will generate a unique number and use this to set up the CSP for the site. This ensures that on every request, the nonce will be unique
  • The client-side code will grab this nonce and add the magic __webpack_nonce__ variable into global scope before any other imports in the app’s entry point file (as defined in the webpack config)

For me, I did this…

(entry point file – index.js)

import React from 'react';
import { Provider } from 'react-redux';
import { render, unmountComponentAtNode } from 'react-dom';
import { ThemeProvider } from 'styled-components';
import '../create-nonce';

import theme from './css/themes/default/theme';
import './css/default/baseline';
//...

The ‘create-nonce’ file needs to come before any imports that reference styled-components, otherwise you’ll get an error. The file itself looks like this…

const styleTag = (document.getElementsByTagName('style') && document.getElementsByTagName('style')[0]) || {};
__webpack_nonce__ = styleTag.nonce;

Here, the file is grabbing the first style tag in the document, which for our site is guaranteed to have the nonce attribute. It then sets the webpack_nonce variable. An alternative would be to simply expose a global variable in the server-side code and set it to the value of the nonce, then reference that from the ‘create-nonce’ file.

This is nice simple solution if you’re using webpack. If you’re not, you’re pretty screwed if you’re trying to use styled-components.

Emotion custom instance

Emotion exposes its core API allowing you to create custom instances. These instances can then be set up to use the nonce value that your server-side code defines. For react styled components, you need to use the ‘create-emotion-styled’ package. Here’s my source code…

import React from 'react';
import createEmotion from 'create-emotion';
import createEmotionStyled from 'create-emotion-styled';

const context = typeof global !== 'undefined' ? global : {};
if (context.__MY_EMOTION_INSTANCE__ === undefined) {
    context.__MY_EMOTION_INSTANCE__ = {};
}

const styleTag = (document.getElementsByTagName('style') && document.getElementsByTagName('style')[0]) || {};
const nonce = styleTag.nonce || 'abc1234';

const emotion = createEmotion(context.__MY_EMOTION_INSTANCE__, {
    nonce,
    key: 'some-value-that-gets-used-in-the-class-names'
}); 

export const {
  flush,
  hydrate,
  cx,
  merge,
  getRegisteredStyles,
  injectGlobal,
  keyframes,
  css,
  sheet,
  caches
} = emotion;

export default createEmotionStyled(emotion, React);

The key bits here are the grabbing of the nonce value, and the ‘createEmotion’ call that uses that nonce value. Now, whenever you want to create a styled component, reference this instance rather than the default ’emotion’ package. e.g…

(component.js)

import styled from 'emotionInstance';
export default styled.div`
  font-size: 1rem;
`;

You’ll notice the reference there to ’emotionInstance.’ This is just a webpack alias I set up to avoid having relative paths all over the place. The only downside here is that mocha (if you’re using that) won’t be able to find the module since it doesn’t know your aliases. I opted for using babel-plugin-webpack-alias which changes your aliased references to relative ones by looking at your webpacks ‘resolve’ object.

The other thing you need to do is set which instances of emotion to use in the babelrc file.

(.babelrc)

"env": {
    "production": {
        "plugins": [
            [
                "emotion",
                {
                    "hoist": true,
                    "instances": ["./emotionInstance"],
                    "primaryInstance": "./emotionInstance"
                }
            ]
        ]
    }
}

There’s a fair few hoops to jump through, but its an alternative to using the automatic webpack_nonce feature, if you’re not using webpack.

Comparing styled components and emotion

Here’s a couple of links I found comparing these

  • https://alligator.io/react/css-in-js-roundup-styling-react-components/
  • https://github.com/emotion-js/emotion/issues/113

As far as I can see, there are a few differences between the 2.

  • Styled components obfuscates classnames so its slightly more difficult to inspect elements. Emotion adds your instance name to the class and provides a ‘label’ parameter that can be used in dev mode to add the component name to the class.
  • ThemeProvider is part of the styled-component’s core package, making the overall size bigger, whereas its separate in emotion.
  • Both allow object styles or tagged template literals
  • Styled components has no concept of instances, whereas emotion allows you to create custom instances
  • Style components is larger than emotion in terms of package size
  • Emotion splits out its components into separate packages, so you can pick and choose more easily which bits you want.

Conclusion

That’s a round-up of some of the pros and cons of css modules, styled components and styled system. In terms of modifiability (if thats a word), use of props to define dynamic styles, theming, and handling logical nesting of tags, there’s a lot to be said for the styled components approach. The next challenge for me is to try and migrate the existing codebase away from css modules. So far, its been pretty easy which is a good indication that styled components represents a logical step forward.

Leave a Reply