RSS

CEK.io

Chris EK, on life as a continually learning software engineer.

A Single Set of Color Vars With PostCSS

In this post, I describe how to create a single list of color variables (in JavaScript) so that those colors can be shared across JavaScript files and CSS stylesheets. Using PostCSS within a Webpack app, I outline the problem of sharing styles between CSS and JS and how it can be solved. For step-by-step code examples, skip ahead to “Enter PostCSS (Problem Solved)”.

The Problem We’re Solving

At my day job, we’d had a colors.css file for a while, where we define all the hex codes for our color scheme, as defined by our designers. It looked something like this:

(colors.css) download
1
2
3
4
5
:root {
  --black: #1d3744;
  --white: #fff;
  --red: #c21e48;
}

This enabled us to use the same colors in any of our other CSS files using CSS modules:

(component-specific.css) download
1
2
3
4
5
6
@import "../theme/colors";

.my-special-component {
  background-color: var(--white);
  color: var(--red);
}

Straightforward, keeps things DRY, makes it easy to change colors when it strikes the designer’s fancy, etc.

For a long time, while our CSS colors were nicely organized, our JS colors weren’t. We have colors in our D3 visualizations and our inline styles on React components. As a simple improvement, I decided to pull all our colors into a single map that could be read by both our CSS and JS files. NB: this post is about CSS colors, but can apply to any CSS variables you’d like shared to JS.

Enter PostCSS (Problem Solved)

PostCSS does a lot of things. I’ll leave it as an exercise to the reader to explore the various plugins (or just some of the most popular ones).

For my purposes, I needed postcss-loader (a loader for Webpack), postcss-cssnext (enables the latest CSS syntax, which we were already using through cssnext), postcss-url, and postcss-import.

The change was effectively X steps (X easy steps to a single list of color variables!):

  1. Add PostCSS plugins to package.json:
    (package.json) download
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    {
      // ...
    
      "devDependencies": {
        // ...
        "postcss-browser-reporter": "^0.5.0",
        "postcss-cssnext": "^2.6.0",
        "postcss-import": "^8.1.2",
        "postcss-loader": "^0.9.1",
        "postcss-reporter": "^1.3.3",
        "postcss-url": "^5.1.2",
        // ...
      },
    
      // ...
    }
    
  2. Since I had previously been using cssnext, I followed [these migration steps](http://cssnext.io/postcss/#postcss-loader) to upgrade to postcss-cssnext. This meant swapping the `cssnext-loader` for `postcss-loader` in my Webpack loaders, removing cssnext from my webpack config, and adding the postcss options to webpack config:
    (webpack.config.js) download
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    module.exports = {
      module: {
        loaders: [
          {
              test:   /\.css$/,
              loader: "style-loader!css-loader!postcss-loader"
          }
        ]
      },
      postcss: function (webpack) {
        return [
          require("postcss-import")({ addDependencyTo: webpack }),
          require("postcss-url")(),
          require("postcss-cssnext")({
            features: {
              customProperties: {
                variables: colorVars // `colorVars` will be defined above, see step 4.
              }
            }
          }),
          require("postcss-browser-reporter")(),
          require("postcss-reporter")(),
        ]
      },
    }
    
  3. Add a colors.js. I chose to CONSTANT_CASE the variables (for idiomatic JS).
    (colors.js) download
    1
    2
    3
    4
    5
    
    module.exports = {
      "BLACK": "#1d3744",
      "WHITE": "#fff",
      "RED": "#c21e48",
    }
    
  4. Require `colors.js` in webpack.config.js.
    (require-colors.js) download
    1
    2
    
    // to be added aboce `module.exports` of webpack.config.js
    var colorVars = require("./app/constants/colors")
    
  5. (OPTIONAL) Because I wanted CSS variables to be lowercase and hyphen-separated so I could maintain our old `color: var(–light-grey);` syntax, I added a transformation from the constant case (using Ramda’s `curry` and `reduce`):
    (case-transform.js) download
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    // to be added above `module.exports` of webpack.config.js
    // color vars in JS are CONST_CASE, but need to be converted to hyphen-case for CSS
    const renameKeys = R.curry((renameFn, obj) => {
      return R.reduce((acc, key) => {
        acc[renameFn(key)] = obj[key]
        return acc
      }, {}, R.keys(obj))
    })
    const constCaseToHyphenCase = (str) => { return str.replace(/_/g, "-").toLowerCase() }
    var colorVars = renameKeys(constCaseToHyphenCase, require("./app/constants/colors"))
    
  6. Go forth and use styles!
    (color-vars.js) download
    1
    2
    3
    4
    5
    6
    
    // we can now do this in React components or any other JS file
    var colors = require("app/constants/colors")
    // ...
    render() {
      return <div style={{backgroundColor: colors.white}} />
    }
    
    (color-vars.css) download
    1
    2
    3
    4
    5
    
    /* and this still works, without even needing to `@import "../theme/colors";` */
    .my-special-component {
      background-color: var(--white);
      color: var(--red);
    }
    

And that’s it! Now, in addition to everything it already did, running webpack-dev-server will (1) compile using PostCSS, (2) read from colors.js, and (3) set all colors in colors.js as global CSS variables.

Limitations

The one limitation is hot-reloading. That is, hot reloading works perfectly on changes to JavaScript files and CSS files, with one exception: colors.js. Since colors.js is read on build, we need to restart the webpack dev server anytime we change or add a color variable. This question poses effectively the same issue (“…every time I change a variable I have to restart the webpack dev server”). For now, that’s a tradeoff I can live with.

Parting thoughts

This new pattern enables much more inline styling with JavaScript. That is, now our React components and D3 visualizations can, in theory, read style variables from JavaScript and never know about CSS.

Following this to its extreme of no-CSS/all-JS may seem crazy, but I remain curious. A lot has been said about how inline styles with JavaScript may be the future. At a minimum, it’s convenient and fun to do more JS and less CSS. I’m excited to see how the community experiments with inline styling and if there come to be best practices around separation of concerns.