Sharing variables between JS and Sass using webpack sass-loader

Paul Thomas
ITNEXT
Published in
5 min readMar 6, 2018

--

Click here to share this article on LinkedIn »

The theme for your project is a file in which you define the various properties that make up its appearance. This might include colors, fonts, breakpoints and sizes/spacing. Having this single source of truth makes updating easy, and means you can be assured that all of your components are getting their values from the same place.

A project may easily end up in a situation where you wish to use a mixture of JS styling (such as styled-components or inline-styles) and Sass styling, but we still wish to have a single source of truth. So how can we go about defining our properties in just one place, but be available to both Sass and JS?

TL;TD

Single Source of Truth

So, we’ve decided that we need to define our properties in one place, but should this be in JS or in Sass? It certainly seems possible to do it in either and I briefly explored both options. I decided that the most logical place to define our rules would be in a JSON object. It’s easy and clear to setup, grouping is intuitive and can easily be manipulated into various formats should it ever be needed elsewhere.

// theme.js
module.exports = {
base: "16px",
spacing: "1rem",
breakpoints: {
xs: "0em" /* 0px */,
sm: "30em" /* 480px */,
md: "64em" /* 1024px */,
lg: "75em" /* 1200px */
},
typography: {
font: "'Open Sans', sans-serif",
text: "1rem",
title: "2rem"
},
colors: {
primary: "#2c97de",
secondary: "#7F8FA4",
warning: "#f2c500",
success: "#1fce6d",
danger: "#e94b35",
error: "#e94b35"
},
};

This theme.js file is a simplified example of what our theme might look like, containing our base size, breakpoints, fonts and colors. This file can then be imported into any JS component that requires it.

Having the values available to Sass

We still need to get these same values into Sass so that we can use them for styling via CSS. We are using webpack to compile our code, and more specifically sass-loader to compile Sass to CSS so that it can be imported into our project via css-loader . There are plenty of articles out there on how to use Sass with webpack, so we won’t delve deeper than that here.

sass-loader uses node-sass and this just so happens to support functions. This means that we can create custom functions that are then accessible via our Sass files. This is what we’ll use to get access to our theme.js properties.

// webpack.config.js...
const sassVars = require(__dirname + "/src/theme.js");
...
module.exports = {
...
{
test: /\.scss$/,
use: [{
loader: "style-loader"
}, {
loader: "css-loader"
}, {
loader: "sass-loader"
options: {
functions: {
"get($keys)": function(keys) {
keys = keys.getValue().split(".");
let result = sassVars;
let i;
for (i = 0; i < keys.length; i++) {
result = result[keys[i]];
}
result = sassUtils.castToSass(result);
return result;
}
}
}
}]
}
...
}

You’ll notice the last line of the function uses a function called castToSass() from node-sass-utils . This handy function converts JS values to Sass values, such as an JSON object to a Sass map, making it possible to use the values from our JS in our Sass.

Using the properties in Sass

This gives us access to a function called get() in our Sass files which can be used like so:

// style.scss
.text {
color: get('colors.secondary');
background: get('colors.primary');
}

This gets us very close to the end goal. I’m not a big fan of the syntax, particularly as within a team environment it would mean training other developers how to use it. I’d much rather use Sass variables and Sass maps. To do this we create a file called variables.scss

$base: get("base");
$spacing: get("spacing");
$grid: get("grid");
$breakpoints: get("breakpoints");
$colors: get("colors");

We then import this file into any Sass file that needs to use those variables.

// style.scss
@import 'variables.scss';
.text {
color: map-get($colors, secondary);
background: map-get($colors, primary);
}

Dealing with dimensions

We’re so very close now, but we have one more battle to fight — dimensions. The code as it is will work and we can say margin: $spacing; and Sass will figure it out. However, what we have really done here is give Sass a string, not a dimension, and if we try and run any Math on it we will get an error. So we really need to convert those strings into dimensions.

node-sass-utils comes to the rescue again here with its SassDimension class.

const convertStringToSassDimension = function(result) {
// Only attempt to convert strings
if (typeof result !== "string") {
return result;
}
const cssUnits = ["rem","em","vh","vw","vmin","vmax","ex","%","px","cm","mm","in","pt","pc","ch"];
const parts = result.match(/[a-zA-Z]+|[0-9]+/g);
const value = parts[0];
const unit = parts[parts.length - 1];
if (cssUnits.indexOf(unit) !== -1) {
result = new sassUtils.SassDimension(parseInt(value, 10), unit);
}
return result;
};

This function checks to see if the string being passed to it is a CSS dimension (ending in px, em, rem, etc.) and if so, creates a SassDimension from the value and denominator so the the castToSass function will be able to correctly interpret the value as a dimension.

We need to run this on each value, and each value in an object, so that we don’t miss any of these values out. Our get() function thus becomes:

"get($keys)": function(keys) {
keys = keys.getValue().split(".");
let result = sassVars;
let i;
for (i = 0; i < keys.length; i++) {
result = result[keys[i]];
if (typeof result === "string") {
result = convertStringToSassDimension(result);
} else if (typeof result === "object") {
Object.keys(result).forEach(function(key) {
const value = result[key];
result[key] = convertStringToSassDimension(value);
});
}
}
result = sassUtils.castToSass(result);
return result;
}

Now we are able to access all of our values either via Sass or JS and they will behave just as if they were originally declared in Sass. We only have to update our theme.js file and we’ll have the updates sitewide — although it’s worth noting that you will need to restart webpack in order for this to work, it doesn’t happen during watch.

I’m Paul Thomas and I’m a Lead Developer at Immersive Labs in Bristol, UK.

--

--