Sass Custom Functions

Lance Bailey
6 min readApr 24, 2022
https://sass-lang.com

TL;DR: The well-documented and commonly suggested methods are patches, there is a better way to do it.

I recently came across a situation where I needed to access configuration defined in JSON from our Scss.

I considered a number of options, one being duplicating the specific parts of the configuration that we need as Scss variables, but this was just not a DRY approach.

In JSON

//devices.json
{
mobile: "320px",
tablet:"600px",
desktop:"960px"
}

Again in Scss

//_devices.scss
$mobile: 320px
$tablet: 600px
$desktop: 960px

Another option I considered was moving the entire configuration into Scss and using Webpack’s sass-loader “:export” block allowing us to access the configuration in Javascript (basically the opposite way around). Yet again, I would be cross-cutting concerns that have no meaning within our world of Scss.

// _devices.scss 
$primary-color: #fe4e5e;
$background-color: #fefefe;
$padding: 124px;
:export {
primaryColor: $primary-color;
backgroundColor: $background-color;
padding: $padding;
}

While these options are well documented and commonly suggested I simply felt that either direction would be a patch and will lead to some really nasty technical debt later down the line.

RTFM

while digging into the sass javascript API to figure out if there was a way to actually talk between our Scss and Javascript, I found a nicely hidden yet obviously useful configuration option. Sass plugin functions!! I jumped for joy but quickly settled down as my colleagues started giving me strange looks and I knew that if our Webpack sass-loader didn't support this option, I was going to have a long night ahead of me trying to make it work.

Webpack and RTFM yet again

Once again, back into reading the docs, I jumped over to Webpack’s sass-loader, scrolled onto the “sassOptions”, and was extremely disheartened to see that their support for functions was not presented there… I did however come across support for fiber, which I remembered reading as part of the Sass plugins, the same plugins that I mentioned earlier for functions. I had a sneaky suspicion that functions would work and had to try it out.

Webpack example of implementing fiber with dart-sass

module.exports = {
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: [
"style-loader",
"css-loader",
{
loader: "sass-loader",
options: {
implementation: require("sass"),
sassOptions: {
fiber: require("fibers"),
},
},
},
],
},
],
},
};

The Prefix

Without going into too much confusing detail, the point here is to load JSON configuration into Scss implementation. This implementation allows us to define a class prefix in our package.json and use it within our Scss.

Sass essentially provides the option of defining a function that will be included during compilation time that can have its logic written in Node.

Two things to note are the input is a Sass Type and the return or output must also be a Sass Type

First things first, let’s write a basic function that we can implement in our Webpack config and start testing.

// src/sass/prefix.js//we require the sass implementation as it should be the same implementation used in webpack config...
module.exports = (sass) => {
//write the most basic function that just returns the input
function prefix(selector) {
return selector;
}
// return an object with the function signature as the key
return {'prefix($selector)': (selector) => prefix(selector)};
}

This is our final Webpack config which includes extracting our CSS into a separate CSS file after Webpack has completed processing.

// webpack.config.jsconst path = require('path');
const sass = require('sass');
// import our custom function
const prefix = require('./src/sass/prefix')(sass);
module.exports = {
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
entry: path.resolve(__dirname, 'src', 'index.scss'),
output: {
path: path.resolve(__dirname, "dist")
},
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: [
{
loader: "file-loader",
options: {
name: '[name].css'
}
},
{loader: "extract-loader"},
{loader: "css-loader"},
{
loader: "sass-loader",
options: {
implementation: sass,
sassOptions: {
functions: {
//include the function
...prefix
}
}

}
}
]
}
]
}
}

Now to test that we can access our function from Scss and that it works as we expect. Remember at this point our function is simply returning whatever we send it. In our case, let's use the Sass @debug function to log the return value when our input value is a string.

// index.scss@debug prefix("lorem ipsum");

lastly, to test this, the easiest way is to add a build script to your npm package.json and run it.

"scripts": {
"watch": "webpack --watch",
"build": "webpack --mode=production"
},

At this point, if everything works your console should print out the following message:

> webpack --mode=productionsrc/index.scss:1 DEBUG: lorem ipsum
[webpack-cli] Compilation finished
asset main.js 556 bytes [compared for emit] [minimized] (name: main)
asset index.css 0 bytes [emitted] [from: src/index.scss] (auxiliary name: main)
runtime modules 1.06 KiB 2 modules
./src/index.scss 53 bytes [built] [code generated]
webpack 5.8.0 compiled successfully in 838 ms
Process finished with exit code 0

I have bolded the relevant line in the output to show that our function works and returns the string we provided in the input. This is an awesome start and means that we can now start doing some fancy work between receiving the input and returning some output.

In our case, the goal will be to define our config in and load our package.json where we will add a prefix, we will then prepend the prefix to the selector and return a Sass String Type.

A great helper tool that I came across is node-sass-utils and it worked seamlessly with the dart-sass implementation.

// import sass-utils and provide it the sass implementation
const sassUtils = require('node-sass-utils')(sass);
//import your package.json which has the scss key
const {scss} = require(path.resolve(__dirname, '../../package.json'));

Now let's improve on the previous prefix function

function prefix(selector) {
//Use assertType to ensure the input is the expected type
sassUtils.assertType(selector, 'string');
//Get your prefix from scss config taken from package.json
const prefix = scss?.prefix ? `${scss.prefix}-` : '';
//prepend the prefix to the selector
const prefixed = `${prefix}${sassUtils.unquote(selector)}`;
//Ensure to cast to Sass before returning
return sassUtils.castToSass(prefixed);
}

Our final piece of code should look like this

// src/sass/prefix.jsconst path = require('path');module.exports = (sass) => {
//import utils and package.json
const sassUtils = require('node-sass-utils')(sass);
const {scss} = require(path.resolve(__dirname,'../../package.json'));

//Our updated function
function prefix(selector) {
sassUtils.assertType(selector, 'string');
const prefix = scss?.prefix ? `${scss.prefix}-` : '';
const prefixed = `${prefix}${sassUtils.unquote(selector)}`;
return sassUtils.castToSass(prefixed);
}
return {'prefix($selector)': (selector) => prefix(selector)};
}

Before we forget, let's add our prefix config to the package.json

// package.json"scripts": {
"watch": "webpack --watch",
"build": "webpack --mode=production"
},
"devDependencies": {
"@webpack-cli/init": "^1.0.3",
"css-loader": "^5.0.1",
"extract-loader": "^5.1.0",
"fibers": "^5.0.0",
"file-loader": "^6.2.0",
"node-sass-utils": "^1.1.3",
"sass": "^1.29.0",
"sass-loader": "^10.1.0",
"webpack": "^5.8.0",
"webpack-cli": "^4.2.0"
},
"scss": {
"prefix": "beyerz"
}

finally let’s create a Scss file where we ensure our final CSS is prefixed using our custom prefix. In my case “beyerz”, but you can choose whatever you like.

// src/index.scss.#{prefix("beyerz")} {
&>.immediate-child{
border: solid 1px #808080;
}
background-color: #ff0000;

&--dark {
background-color: darken(#ff0000, 0.5);
}

&--light {
background-color: lighten(#ff0000, 0.5);
}
}

Once we run the build and look into our “dist” you will see the compiled CSS with your chosen prefix!

.beyerz-sample {
background-color: red
}

.beyerz-sample > .immediate-child {
border: solid 1px gray
}

.beyerz-sample--darken {
background-color: #fc0000
}

.beyerz-sample--lighten {
background-color: #ff0303
}

I hope you enjoy creating custom functions and letting your creativity go wild…!!

--

--