How we use Sass and Gulp in our WordPress theme & plugin development workflow

It’s been quite a while now since we’ve completely abandoned vanilla CSS for a CSS preprocessor (more than four years actually) and more specifically for Sass (with SCSS syntax). There are quite a few reasons why we did that, and the main one is improved DX (developer experience) along with easier plugin integrations.

Simply put, Sass used to be (and still is) a much more powerful language than vanilla CSS, especially if you’re concerned with older browser support (i.e… IE ¯\_(ツ)_/¯). I’ve found that the need for variables, conditionals, mixins, rule nesting, along with color functions and all the goodies a preprocessor comes with is much more apparent within the WordPress theming context where you wish to provide multiple color schemes for your theme or tame popular WordPress plugins to match your theme’s design and layout.

That said, I’m not here to sell you on CSS preprocessors right now, this piece is an overview on how we use Sass with Gulp in our theme and plugin development workflow, and how to incorporate the same workflow to your theme, if you so wish.

Our specific requirements

Back when we started exploring how we’d go about integrating Sass in our themes we had a few hard requirements that usually do not apply to other projects (like standalone apps or websites). This is because our customers range from simple website owners with little to no CSS knowledge to website developers with varying skillsets (not everyone is a front end developer). The above means that:

  • All our themes must provide a complete vanilla CSS stylesheet (the main theme’s stylesheet – style.css) which mustn’t look like it was compiled from something else (to the best extend possible) and, more crucially, be readable. A lot of people simply want to add a few touches here and there or copy and paste selectors in their child theme’s stylesheet; they shouldn’t have to deal with Sass if they don’t want to.
  • We do not ship themes with a minified stylesheet, for obvious reasons like mentioned above (also it’s not really allowed by the WordPress theme guidelines).
  • All vendor stylesheets (e.g. from jQuery plugins) should be in their separate files and not included in the main theme’s stylesheet which must contain only theme styles.
  • Sass should be transparent: structure our file hierarchy so that our users can work both with Sass or vanilla CSS depending on experience and preferences.

With the above in mind we’ve settled down in the following file structure with regards to stylesheets:

├── css (All CSS/Sass files except the main one are included here)
│   ├── inc (Sass partials - theme styles only)
|   |   ├── _base.scss
|   |   ├── _header.scss
|   |   ├── _footer.scss
|   |   ├── _modules.scss
|   |   ├── _woocommerce.scss
|   |   └── ...etc
│   ├── font-awesome.scss
│   ├── font-awesome.css
│   ├── magnific.scss
│   ├── magnific.css
│   ├── mmenu.scss
|   ├── mmenu.css
│   └── ... other vendor stylesheets
├── fonts
├── images
├── js (All JavaScript files)
├── ...
├── ...
├── style.css (Compiled theme styles)
└── style.scss

This allows us to keep Sass merely as an enhancement for the end user that prefers CSS, and still be prominent enough to be used if someone else so chooses.

All compiled files are right next to their sources and each one can easily be ignored in favor of the other. Also notice we’ve stuffed all Sass partials in the /css/inc directory exactly so that they can be out of the way, if required.

The main style.scss file simply imports all partials, and contains no actual styles in it. For example:

/*
Theme Name: Demo theme
Theme URI: http://www.cssigniter.com/themes/demotheme
Author: CSSIgniter
Author URI: http://www.cssigniter.com
Description: A theme description
Version: 1.0
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
*/

/* -----------------------------------------
	Table of Contents
--------------------------------------------

.. 01. General
.. 02. Header
.. 03. Modules
.. 04. Footer
.. 05. Comments
.. 06. Widgets Styling
.. 07. WordPress defaults
.. 08. Utilities
*/

@import 'css/inc/variables';
@import 'css/inc/mixins/mixins';
@import 'css/inc/base';
@import 'css/inc/header';
@import 'css/inc/modules';
@import 'css/inc/footer';
@import 'css/inc/comments';
@import 'css/inc/widgets';
@import 'css/inc/wp-defaults';
@import 'css/inc/utilities';
example style.scss file

Compiling Sass with Gulp

Gulp usually needs no introductions, but for the sake of completeness, it’s a task runner tool for any kind of task automation in development workflows, and it’s blazing fast at it. It can handle virtually any task with its powerful plugin architecture; from JavaScript or CSS compilation and minification to actual deployments or other more complex tasks. It is JavaScript based and runs on Node.js.

We use Gulp extensively at CSSIgniter. We have over a dozen different Gulp tasks in our internal toolkit for our various workflows (which we’ll cover in another tutorial), one of them being Sass compilation which we’ll glance over right away.

Getting started

All Gulp needs to run is to have Node.js installed. After that we’ll need to initialize a project with npm. First create a directory where your project will live in with the structure we’ve mentioned in the previous section and initialize the project:

npm init
Initializing a Node.js project with npm

A package.json file is immediately created for us, which is used to keep track various information about the project such as its dependencies.

For our Sass toolkit we’ll need to install the following dependencies:

npm install --save-dev gulp gulp-plumber gulp-sass gulp-postcss autoprefixer gulp-group-css-media-queries browser-sync
Installing dependencies

Having done that, let’s also globally install Gulp on our system so that we can actually run it as a command:

# note: you might need to run this as "sudo"
npm install gulp --global

Another note: If you don’t want to pollute your system with global dependencies (and you shouldn’t) you can run gulp as an npm script in package.json. Under "scripts" add: "gulp": "gulp". Now running npm run gulp taskname will be the same as running gulp taskname.

After that we’ll need to create a file named gulpfile.js. This will be our toolkit’s main file, as Gulp expects and reads this in the current working directory any time we run gulp from the command line.

Inside gulpfile.js, require our dependencies:

const gulp = require('gulp');
const plumber = require('gulp-plumber');
const sass = require('gulp-sass');
const postcss = require('gulp-postcss');
const autoprefixer = require('autoprefixer');
const sourcemaps = require('gulp-sourcemaps');
const groupmq = require('gulp-group-css-media-queries');
const bs = require('browser-sync');
gulpfile.js

And our Sass compilation task:

const gulp = require('gulp');
const plumber = require('gulp-plumber');
const sass = require('gulp-sass');
const postcss = require('gulp-postcss');
const autoprefixer = require('autoprefixer');
const sourcemaps = require('gulp-sourcemaps');
const groupmq = require('gulp-group-css-media-queries');
const bs = require('browser-sync');

const SASS_SOURCES = [
  './*.scss', // This picks up our style.scss file at the root of the theme
  'css/**/*.scss', // All other Sass files in the /css directory
];

/**
 * Compile Sass files
 */
gulp.task('compile:sass', () =>
  gulp.src(SASS_SOURCES, { base: './' })
    .pipe(plumber()) // Prevent termination on error
    .pipe(sass({
      indentType: 'tab',
      indentWidth: 1,
      outputStyle: 'expanded', // Expanded so that our CSS is readable
    })).on('error', sass.logError)
    .pipe(postcss([
      autoprefixer({
        browsers: ['last 2 versions'],
        cascade: false,
      })
    ]))
    .pipe(groupmq()) // Group media queries!
    .pipe(gulp.dest('.')) // Output compiled files in the same dir as Sass sources
    .pipe(bs.stream())); // Stream to browserSync
gulpfile.js

This is very close to what we use for our themes. Now running gulp compile:sass (or npm run gulp compile:sass) in the command line at our project’s root will handle all our Sass compilation. Let’s go over the key features:

...
    .pipe(sass({
      indentType: 'tab',
      indentWidth: 1,
      outputStyle: 'expanded', // Expanded so that our CSS is readable
    })).on('error', sass.logError)
...
Sass compilation task

This is the key ingredient of our task, enabled by the gulp-sass module. It’s where Sass actually gets compiled to CSS. We properly indent and output in expanded style so that we meet our readability requirements.

...
    .pipe(postcss([
      autoprefixer({
        browsers: ['last 2 versions'],
        cascade: false,
      })
    ]))
...
Autoprefixer

Autoprefixer is such an amazing tool. As the name implies, it automatically adds browser/vendor prefixes to our code for just the browser versions we’ve specified (in this particular example it would be the last 2 versions of all browsers). This allows us to freely write CSS without ever having to remember to add prefixes by ourselves (e.g. on flexbox) while maintaining support for older browsers that require them.

One more benefit is that when we decide to drop support for a particular browser (or as browsers advance in versions), all we need to do is remove that browser from our gulpfile, recompile the CSS, and all relevant prefixed statements will be gone!

...
    .pipe(groupmq())
...
Grouping CSS media queries

Another God-sent tool: gulp-group-css-media-queries. To understand why we need it let’s see how we usually write CSS Media Queries with Sass:

.foo {
  font-size: 18px;

  @media (min-width: 767px) {
    font-size: 24px;
  }
}

.bar {
  margin-bottom: 20px;

  @media (min-width: 767px) {
    margin-bottom: 30px;;
  }
}
Nested Media Queries with the power of Sass

Nesting Media Queries under their related elements is very handy, it allows us to encapsulate every style under each selector. The problem with this approach is that it gets compiled into:

.foo {
  font-size: 18px;
}

@media (min-width: 767px) {
  .foo {
    font-size: 24px;
  }
}

.bar {
  margin-bottom: 20px;
}

@media (min-width: 767px) {
  .bar {
    margin-bottom: 30px;
  }
}
CSS output

Which has a few problems. Mainly it creates a new Media Query block for each declaration, increasing filesize and impacting readability significantly.

With group-css-media-queries we avoid all these issues. The plugin simply traverses our code and groups together all matching Media Queries at the end of our stylesheet, so that the output would become:

.foo {
  font-size: 18px;
}

.bar {
  margin-bottom: 20px;
}

@media (min-width: 767px) {
  .foo {
    font-size: 24px;
  }
  
  .bar {
    margin-bottom: 30px;
  }
}

Efficient, and much cleaner!

Our final line, streams the results to browserSync, which we’ll cover right away, after we create a new task which will start watching our Sass files for any changes and immediately compile and stream them to the browser.

...

/**
 * Watch Sass files for changes
 */
gulp.task('watch:sass', ['compile:sass'], () => {
  bs.init({
    proxy: 'http://localhost/wordpress-installation'
  });

  gulp.watch(SASS_SOURCES, ['compile:sass']);
});
gulpfile.js

This task actually does a bit more: First it initializes a browserSync server instance. BrowserSync is another amazing tool with a lot of uses.

In this particular scenario we use it as a proxy to our local WordPress development server (which in this case is under http://localhost/wordpress-installation — replace this with your own local dev URL). That way we can automatically stream all file changes to browserSync which will handle reloading the page for us, or in the case of CSS, it’ll directly and transparently inject the new styles without even reloading! A huge boost in productivity all around.

Then we simply watch all Sass files for changes with Gulp’s internal API (gulp.watch), triggering the compile:sass task (which then streams the changes to browserSync, which then injects the styles).

And to conclude, adding a gulp default task:

...

/**
 * Default task executed by running `gulp`
 */
gulp.task('default', ['watch:sass']);
gulpfile.js

Now any time we run just gulp, the watch:sass task will trigger since we’ve declared it as the default one.

Bonus: Linting our styles with sass-lint

No development workflow would be complete without a linter. Linting code has huge benefits, it prevents mistakes and helps maintain a concise and uniform codebase. Adding linting to the above workflow is as simple as creating a new task. Let’s first install the required dependency:

npm install gulp-sass-lint --save-dev

And append the lint task in our gulpfile.js:

...

const sassLint = require('gulp-sass-lint');

...

gulp.task('compile:sass', ['lint:sass'], () =>

...

gulp.watch(SASS_SOURCES, ['compile:sass', 'lint:sass']);

...

/**
 * Lint Sass
 */
gulp.task('lint:sass', () =>
  gulp.src(SASS_SOURCES)
    .pipe(plumber())
    .pipe(sassLint())
    .pipe(sassLint.format()));

...
gulpfile.js

Notice that we’ve also made the new lint:sass task running at various parts of our workflow. Now every time we either compile Sass files or watch for changes the linter will automatically trigger.

Note: You can use the sample sass-lint config file to create your own linting rules.

And the full gulpfile.js:

const gulp = require('gulp');
const plumber = require('gulp-plumber');
const sass = require('gulp-sass');
const postcss = require('gulp-postcss');
const autoprefixer = require('autoprefixer');
const sourcemaps = require('gulp-sourcemaps');
const groupmq = require('gulp-group-css-media-queries');
const bs = require('browser-sync');

const SASS_SOURCES = [
  './*.scss', // This picks up our style.scss file at the root of the theme
  'css/**/*.scss', // All other Sass files in the /css directory
];

/**
 * Compile Sass files
 */
gulp.task('compile:sass', ['lint:sass'], () =>
  gulp.src(SASS_SOURCES, { base: './' })
    .pipe(plumber()) // Prevent termination on error
    .pipe(sass({
      indentType: 'tab',
      indentWidth: 1,
      outputStyle: 'expanded', // Expanded so that our CSS is readable
    })).on('error', sass.logError)
    .pipe(postcss([
      autoprefixer({
        browsers: ['last 2 versions'],
        cascade: false,
      })
    ]))
    .pipe(groupmq()) // Group media queries!
    .pipe(gulp.dest('.')) // Output compiled files in the same dir as Sass sources
    .pipe(bs.stream())); // Stream to browserSync

/**
 * Start up browserSync and watch Sass files for changes 
 */
gulp.task('watch:sass', ['compile:sass'], () => {
  bs.init({
    proxy: 'http://localhost/wordpress-installation'
  });

  gulp.watch(SASS_SOURCES, ['compile:sass', 'lint:sass']);
});

/**
 * Lint Sass
 */
gulp.task('lint:sass', () =>
  gulp.src(SASS_SOURCES)
    .pipe(plumber())
    .pipe(sassLint())
    .pipe(sassLint.format()));

/**
 * Default task executed by running `gulp`
 */
gulp.task('default', ['watch:sass']);
gulpfile.js

And that’s about it! As an exercise to the user I’ll leave it up to you to add sourcemap support or other goodies such as more advanced PostCSS plugins!

Do you use a CSS preprocessor in your WordPress projects? How about Gulp? Let us know of your experiences, tips, or tricks in the comments below.

Subscribe to our newsletter.

Get fresh WordPress content straight into your inbox. We hate spam more than you do.

2 comments

  1. Interesting insights Vassilis. Have you had a chance to check WPGulp? I built it about four years ago and have iterated on it a lot. 500+ devs use it.

    Take a look: https://github.com/ahmadawais/WPGulp

    1. Vassilis Mastorostergios says:

      Hey Ahmad! Thanks for your comment. I have to admit it’s the first time I have a look at WPGulp and it looks like a great tool. Our Gulp workflow is very similar although we do not minify styles and scripts since our products are extensible themes. Keep it up!

Leave a Reply

Your email address will not be published. Required fields are marked *