Stylish Javascript - Static CSS-in-JS and CSS Modules

Stylish Javascript: Static CSS-in-JS and CSS Modules

CSS, our dear old friend, stubbornly refuses to be a programming language.

CSS, as used in practice, is very clever. We preprocess it, postprocess it and modularise it. But JavaScript, as used in practice, is becoming exceedingly clever at composing objects.

CSS, being almost but not exactly like JSON, very often looks like a problem of composing objects.

So it turns out to be useful to build your CSS with JavaScript.

Uh…so throw out all our CSS and start over?

Your legacy code was in the way of my new toy

You could do that! My colleague Mark Dalgleish has an exciting overview of the rapidly evolving CSS-in-JS world, describing a wealth of libraries and frameworks to supercharge your styling. There’s certainly many advantages to picking one and propagating it through your component tree.

My team is opting for a lighter touch; we have a wealth of UI components using CSS Modules with LESS - for examples see the open source SEEK style guide. We have additional requirements, such as multi-branding and internationalisation, that JavaScript composition is well suited to, but no real desire to discard a bunch of recently authored and high-quality LESS.

Just another CSS Module

With the help of Mark and the SEEK Group frontend practice, we settled on a plan of importing our styles authored in JS into our components as a CSS module as normal. This has two key advantages:

  1. It puts the decision to author in JS on the same level as deciding to use a given preprocessor. It fits smoothly into our well established patterns, has no impact on the end user and is transparent to our components.
  2. There’s no rule forbidding a component from importing multiple CSS modules. You can introduce new JS-authored styles in parallel to your LESS (or other CSS confabulator of choice) styles.

For example, say we have a <Button /> component:

import styles from './Button.less';

export default ({ text, handleClick }) => {
  return (
    <div className={styles.root}>
        <button className={styles.button}
                onClick={handleClick}>
            {text}
        </button>
    </div>
  )
}

Let’s assume styles.root handles spacing and layout concerns while styles.button has our colours, typography etc. The former are constant, the latter vary by brand.

One of the brands we’re working with, AwesomeBrand, have undergone a particularly…brave visual refresh and need their styles differentiated.

We’ll make a brand-aware module to export our palette:

import { currentBrand, AWESOME_BRAND } from 'some-module';

const awesomeBrandCallToAction = {
    backgroundColor: 'garishGreen',
    color: 'luridYellow'
};

export const callToAction = {
    backgroundColor: 'pink',
    color: 'white',
    ...(currentBrand === AWESOME_BRAND ?
        awesomeBrandCallToAction : {} )
};

And our typography:

import { currentBrand, AWESOME_BRAND } from 'some-module';

const awesomeBrandStandardType = {
    fontSize: '36px',
    lineHeight: '72px',
    '@media (min-width: 1024px)': {
        fontSize: '72px',
        lineHeight: '144px'
    }
};

export const standardType = {
    fontSize: '18px',
    lineHeight: '18px',
    ...(currentBrand === AWESOME_BRAND ?
        awesomeBrandStandardType : {})
};

In our Button component’s directory, we create a Button.css.js file that composes these styles:

import { callToAction } from 'palette';
import { standardType } from 'typography';

export default {
    '.button': {
        ...callToAction,
        ...standardType
    }
}

And then we refactor our Button.js component file proper to make use of it:

import styles from './Button.less';
import brandStyles from './Button.css';

export default ({ text, handleClick }) => {
  return (
    <div className={styles.root}>
        <button className={brandStyles.button}
                onClick={handleClick}>
            {text}
        </button>
    </div>
  )
}

Our button will render with the same spacing rules composed in LESS on the styles.less class, but with the brand aware styles composed in JavaScript for the brandStyles.button class. Which, if the application was started in AwesomeBrand mode, would mean lurid yellow 36px text.

Great, but how? (or enter css-in-js-loader)

It's magic

Of course, like so much of our modern day frontend black magic, this won’t actually work without a little webpack configuration. The point of the exercise is to have a CSS module, not a JavaScript object literal (JavaScript was just our tool of choice for composing it), so we need to load our css.js files into our CSS pipeline.

I’ll use a simplified version of the configuration in our frontend tooling sku to illustrate. Say our webpack config looked a little like this:


const jsLoaders = [{
   //Whatever babel etc. config you need
}];

const cssLoaders = [{
    loader: 'css-loader'
},
{
    loader: 'postcss-loader'
},
{
    loader: 'less-loader'
}];

const module = {
    rules: [{
        test: '/\.js$/',
        use: jsLoaders
    },
    {
        test: '/\.less$/',
        use: cssLoaders
    }]
};

//More code that eventually outputs a webpack config object

The problem here is that our css.js files will go through the JS loaders, but not the CSS ones. We want them to go through both.

We’re using css-in-js-loader to transform our css.js files into CSS, ready for the CSS loaders. To do that we’ll:

  1. Refactor our cssLoaders array into a function that returns an array optionally including the loaders we need for CSS-in-JS
  2. Add a rule that matches css.js and calls that function with the appropriate parameter
  3. Exclude css.js from our normal .js rule

The result looks like this:


const jsLoaders = [{
   //Whatever babel etc. config you need
}];

const makeCssLoaders = ({ js }) => {

    const cssInJsLoaders = [{
        loader: 'css-in-js-loader'
    },
    ...jsLoaders];

    return [{
       loader: 'css-loader'
    },
    {
       loader: 'postcss-loader'
    },
    {
       loader: 'less-loader'
    },
    ...(js ? cssInJsLoaders : [])  //Needs to be last
    ];

};

const module = {
    rules: [{
        test: '/(?!\.css)\.js$/',
        use: jsLoaders
    },
    {
        test: '/\.css\.js$/',
        use: makeCssLoaders({ js: true })
    },
    {
        test: '/\.less$/',
        use: makeCssLoaders()
    }]
};

//More code that eventually outputs a webpack config object

Now our app will support the amazing AwesomeBrand experience we built above (disclaimer: AwesomeBrand does not represent the many talented design and product professionals working with the GDP and the SEEK group. It may represent what would happen if I was obliged to produce UI without their help).

Is it worth it?

What has our science wrought

Garish green buttons with 72px typography probably aren’t, no.

The approach to CSS-in-JS detailed here is, if your style composition requirements are heavy enough to benefit from the power of a full programming language and its ecosystem. You’ll note even these simplified examples are making heavy use of modules, destructuring and spread (even conditional spread)! You don’t need to use all of those to justify it - maybe the humble switch is enough to get you over the line - but you do need a problem that your team feels is too unwieldy for preprocessors.

The GDP are past masters of multi-branding and use a variety of techniques to solve it according to the needs of the product. But I’m excited to be adding CSS-in-JS to that arsenal - because JavaScript is gaining capability so explosively, I’m of the opinion it’ll be our big gun for the really thorny composition problems.


Jye Nicolson is a Senior Developer at SEEK International, working with the GDP and SEEK Asia. He likes hipster frontend technologies, hipster food and hipster coffee, but insists none of that makes him a hipster.

Header/Thumbnail image: KUKA Industrial Robots IR, Mixabest, Wikimedia commons

© 2018 Global Delivery Pod. All Rights Reserved