DEV Community

Michael Scherr
Michael Scherr

Posted on • Updated on

Switching from SASS to PostCSS

Ever since PostCSS came out, I've exclusively used it for Autoprefixer. I didn't really dig into the power of PostCSS and how it could actually be a comparable solution to SASS.

This article will go into depth about my skepticisms, and how I was able to find solutions using PostCSS.

The Push

What originally considered me to switch was my interest in Tailwind CSS. I love the ideas of utility libraries, but found it difficult to sell to other developers. However, we've been looking for ways to optimize development time, so Tailwind felt like a good choice. It's built with PostCSS in mind, so I at least wanted to give it a try.

Previous Approach

My previous approach was a mix of SASS, BEM, ITCSS, & SMACSS. They solved the following issues:

  • SASS - variables, loops, imports, nested selectors, etc
  • BEM - reduce specificity and allow for easier additions / modifications to code
  • ITCSS - (same as BEM), but also allows for an inverted triangle hierarchy for specificity gets more and more specific.
  • SMACSS - used mostly for modifier classes, like .js-is-active, .has-posts, etc.

This worked really well for me. I saw the power of utility libraries, but figured I would let them evolve and try them out later.

The Switch

I won't bore you with the details, but I went through many iterations like:

  • PostCSS only, no plugins besides Tailwind
  • PostCSS + SASS
  • PostCSS only, with Tailwind and other complementary plugins

The core issue with the PostCSS + SASS approach was the inevitable duplication. For example, these situations can't work together:

/**
  * cannot set $font-size-px-base in PostCSS
  * since SASS gets run first, so would have
  * to duplicate code
*/
@function rem($pixels, $context: $font-size-px-base) {
  @if (unitless($pixels)) {
    $pixels: $pixels * 1px;
  }
  @if (unitless($context)) {
    $context: $context * 1px;
  }
  @return $pixels / $context * 1rem;
}

/**
  * can't use Tailwind variables
  * in the SASS function `scale` since
  * SASS runs before PostCSS
*/
.selector {
  font-size: scale(font-size, theme('fontSize.lg'), theme('fontSize.xl'));
}

/**
  * Interpolation was also a nightmare
  * I couldn't figure out how to make
  * something like this work
  * even with PostCSS Simple Vars
*/
.selector {
  $path: theme('path.theme');

  background-image: url($(path)/test.png);
}

Final Solution

The final solution involved PostCSS only, with Tailwind and other complementary plugins.

When I thought about it, I was really using SASS for:

  • imports
  • mixins
  • functions
  • variables
  • nested selectors - rarely, but nice to have, especially for pseudo selectors
  • loops - very rarely, but nice to have

PostCSS has solutions for all of these once I realized the power of their plugin system. Furthermore, I found out some plugins, like for functions and mixins, allow you to inject Javascript functions into PostCSS. I was sold.

Imports

PostCSS Imports gives the exact same functionality as SASS Imports, so it was an easy switch.

@import 'tailwindcss/base';

@import 'tailwindcss/components';
@import 'components/btn';

@import 'tailwindcss/utilities';

Functions

PostCSS Functions allows you to write functions IN JAVASCRIPT and inject them into PostCSS. IN JAVASCRIPT. I LOVE JAVASCRIPT.

// functions.js

let settings = require('./settings');

let rem = (pixels, context = settings.defaults.fontSizePxBase) => {
  pixels = parseFloat(pixels);

  let result = pixels / context;

  return `${result}rem`;
};

module.exports = {
  rem,
};

Mixins

PostCSS Mixins is the same deal as PostCSS Functions.

// mixins.js

let placeholder = (mixin, immediateSelector = true) => {
  let vendors = [
    '::-webkit-input-placeholder',
    ':-moz-placeholder',
    '::-moz-placeholder',
    ':-ms-input-placeholder',
  ];

  return vendors.reduce((prev, vendor) => {
    let selector = immediateSelector ? `&${vendor}` : vendor;

    prev[selector] = {
      '@mixin-content': {},
    };

    return prev;
  }, {});
};

module.exports = {
  placeholder,
};

Other Notable Plugins

Example PostCSS Config File

// postcss.config.js

let functions = require('./functions');
let mixins = require('./mixins');

module.exports = function(context) {
  /**
   * context comes from what you pass
   * into `gulp-postcss`, omitting detailing here
   * since `gulp` is an implementation detail
   */
  let { options } = context;

  let plugins = [
    require('postcss-import')({
      path: [options.paths.components, options.paths.styles, 'node_modules'],
    }),
    require('tailwindcss')('./tailwind.config.js'),
    require('postcss-functions')({
      functions,
    }),
    require('postcss-mixins')({
      mixins,
    }),
    require('postcss-nested'),
    require('postcss-simple-vars'),
    require('autoprefixer'),
  ];

  return {
    plugins,
  };
};

I hope this at least got you interested in PostCSS and what it's powerful plugin system can provide for your next project.

Discussion (3)

Collapse
michaelhays profile image
Michael Hays

Nice write-up! Check out postcss-preset-env, which does the same thing as a lot of your plugins, in just one: preset-env.cssdb.org/

Collapse
michaeldscherr profile image
Michael Scherr Author

Thanks! I'll check it out soon.

Collapse
epsi profile image
E.R. Nurwijayadi

Fascinating explanation.

I also make article about PostCSS.

epsi-rns.gitlab.io/frontend/2019/1...

PostCSS Loop Spacing Helper