PowerUp your CSS Mixins by writing them with Javascript

Wait, what? But SCSS / Postcss-mixins does what I want. It’s CSS logic, it should be on the CSS side.

Are you sure? Maybe you should think bigger. Have you ever wished for:

  • Faster compilation time
  • Prettier and more advanced logic than @if | @for | $list
  • Access to values outside the CSS files
  • Unit testing — yes, put your CSS to the test!

Sounds cool, right? Let me show you an example with… a button! Who doesn’t need buttons in their projects? We all need them.

We know that WET it’s not scalable and DRYing the code is the way, so let’s use @mixins to make this easier. Note that I’m using PostCSS-mixins.

Scenario 1 — a basic button @mixin

index.css

@define-mixin button $bg, $color, $bgHover {
background: $bg;
border-color: $color;
color: $color;
    &:hover, &:focus {
background: $bgHover;
}
}
.button {
/* all those boring common styles */
    &-default { 
@mixin button teal , white, tomato;
}
    &-mono { 
@mixin button white, gray, darkGray, white;
}
    /* &-bring-the-gang { 
you already lost count
} */
}

Writing Mixins with Javascript sounds odd at first, but then you just wonder why haven’t you always written them down like this.

You write a function that gets the arguments like in CSS. Then you return the CSS as an object. It’s pretty much the same as CSS: a key (property) and a value.

Plus, you don’t need to change nothing on your CSS besides deleting the mixin that we have written before. You can also delete the plugin postcss-simple-vars from postcss.config.js that you were using with postcss-mixins.

index.js

button(bg, color, bgHover) {
return {
background: bg,
color: color,
    '&:hover, &:focus' {
background: bgHover,
}
};
}

I admit, this scenario might show nothing new compared with the PostCSS-mixin. Let me show you other scenarios:

Scenario 2— access an object

// ----- index.js ----- //
const paddingSizes = {
sm: '0.2rem 0.5rem',
md: '0.5rem 1rem',
lg: '1rem 2rem',
}
padding(size) {
return {
padding: paddingSizes[size],
}
}

/* ----- index.css ----- */
.item {
@mixin padding md; /* output -> padding: 0.5rem 1rem; */
}

Scenario 3— math

// ----- index.js ----- //
fontSize(px) {
return {
'font-size': (px / 16) + 'rem',
}
}
/* ----- index.css ----- */
.item {
@mixin fontSize 20; /* output -> font-size: 1.25rem; */
}

Do you see now what Javascript can offer us here?

You could save all your values in an object and then loop through them. You could split your big CSS mixin into smaller readable functions. You could manipulate the parameters better. After all, you have the Javascript power in your CSS code.


Unit Testing

Even if your @mixins are simple, you want to make sure nothing will break them down. And that’s where Unit Test comes into play.

Guess what, to test a mixin is as simple as testing any other function. Here I’m using Jest that uses snapshots to validate the test.

mixin.test.js

test('font-size render with 20px - 1.25rem', () => {
expect(mixins.fontSize(mock, '20')).toMatchSnapshot();
});
Snapshot tests are a very useful tool whenever you want to make sure your UI does not change unexpectedly. (…) A typical snapshot test case (…) takes a screenshot, then compares it to a reference image stored alongside the test. The test will fail if the two images do not match: — Snapshot Testing . Jest

minix.test.js.snap — generated

exports[`test font-size render with 20px - 1.25rem 1`] = `
Object {
"font-size": "1.25rem",
}
`;

Integration

Supposing you already have PostCSS with PostCSS-Mixins, you are good to go. Otherwise, this is the part where you can check this repository with the final result.

mixins/index.js

// PostCSS is needed to make this work
const postcss = require('postcss');
const paddingSizes = {
sm: '0.2rem 0.5rem',
md: '0.5rem 1rem',
lg: '1rem 2rem',
}
const mixins = {
    // The first argument, mixin, is used to connect postCSS-mixins
button(mixin, bg, color, bgHover) {
return {
background: bg,
color: color,
'&:hover, &:focus': {
background: bgHover,
}
};
},
padding(mixin, size) {
return {
padding: paddingSizes[size],
}
},
fontSize(mixin, px) {
return {
'font-size': (px / 16) + 'rem',
}
}
};
module.exports = mixins;

postcss.config.js
Now comes the secret part: You connect your mixins.js to PostCSS to be able to read them on the CSS side.

const mixins = require('./src/styles/mixins/');
module.exports = {
plugins: [
/* ... */
require('postcss-mixins')({
mixins: mixins,
}),
/* ... */
]
};

mixins.test.js

const mixins = require('./../src/styles/mixins');
const mock = {
replaceWith: jest.fn(),
};
test('button default generates with correct styles', () => {
expect(mixins.button(mock, 'teal', 'white', 'tomato')).toMatchSnapshot();
});
test('padding generates with small size', () => {
expect(mixins.padding(mock, 'sm')).toMatchSnapshot();
});
test('font-size render with 20px - 1.25rem', () => {
expect(mixins.fontSize(mock, '20')).toMatchSnapshot();
});

Yeah, that’s it! Now you can write all your mixins in Javascript with more control over what you need to accomplish and without fearing someone else breaking them. It’s a win win for both sides!


I made a repository on Github with these working examples. 
I hope you enjoy it!