Using Tailwind CSS with Sage 9 + Webpack

I am looking to use Tailwind CSS as my front end framework on a new Sage 9 project. Tailwind is essentially a PostCSS plugin, so ultimately I am trying to find the best way to add a PostCSS plugin into Sage’s Webpack build configuration. I’m not very familiar with Webpack, but after some tweaking I was able to get it working, albeit a little sloppy.

With some debugging, I discovered that Webpack will pick up on a PostCSS config file if it is placed inside the assets/styles directory. My postcss.config.js file now looks like this:

module.exports = {
    plugins: [
        require('tailwindcss')('resources/assets/styles/tailwind.js')
    ]
}

(tailwind.js is the Tailwind CSS config file).

This works, but I get a warning about sourcemaps during build and I’d like to move the config files out of the styles directory and into the build directory. This is where I am stuck, as Sage doesn’t seem to use PostCSS as described in their docs. Anyone get this working in a more clean way?

I don’t have an exact answer but I am interested in this too and I saw a post a while ago about replacing Sage 9’s build system with Laravel Mix - maybe give that a go since Laravel Mix seems to be better supported by Tailwind CSS.

Good luck :slight_smile:

Thanks for the reply. I did see that as well, in fact there is a pull request from the authors which indicates they’re interested in using Mix as well. From the PR though, it looks like build times with Mix are 10-20s slower. Not sure it’s worth the slower build times considering I got it working as-is, albeit a bit sloppy.

I hadn’t seen that pull request or all of the surrounding discussions but until the build speed can be fixed, it sounds like it would be more painful than helpful… On the positive side, with Tailwind, there should be less need for constant CSS rebuilds :slight_smile:

@samrap Could you share some more of your setup for getting tailwind setup with sage?

Been looking at using tailwind but it looks like it could be a pain to set up. would you be able to share your file structure and did you get around the sourcemaps warning?

Sage loads PostCSS plugins with postcss-loader. Adding tailwindcss to the plugin list and then specifying the path to the config as a string works. Here’s the default postcss.config.js with the described change:

/* eslint-disable */

const cssnanoconfig = {
  preset: ['default', { discardcomments: { removeall: true } }]
};

module.exports = ({ file, options }) => {
  return {
    parser: options.enabled.optimize ? 'postcss-safe-parser' : undefined,
    plugins: {
      // I’m using the to get the assets path (see config.js),
      // but you could just use the absolute path instead.
      tailwindcss: `${options.paths.assets}/styles/tailwind.js`, 
      cssnano: options.enabled.optimize ? cssnanoconfig : false,
      autoprefixer: true,
    },
  };
};

Also, you’ll need to add tailwind to stylelint’s ignoreAtRules list in package.json so linting styles doesn’t throw errors at you (h/t @austenc via his issue on the sage-installer repo).

Now add PurgeCSS and you’ve got an awesome setup for designing with speed whilst keeping your stylesheet tiny (when building for production).

3 Likes

Thanks @knowler for this is there a step by step guide anywhere of how to set all this up struggling to get it to work with a default sage download (I’ve never actually used tailwind or postcss just the sass setup and tachyons before but tailwind seems more advanced. I saw @austenc throwing the idea of a guide around?

Thanks

How to setup Tailwind CSS in Sage 9

I :heart: designing in the browser. Luckily, utility-first CSS frameworks like Tailwind make it easier than ever. Here’s how you can get started using Tailwind CSS with Sage 9.

During the Sage install choose No Framework (you don’t have to, but it will help avoid naming conflicts + help reduce filesize), then complete the following:

1. Install Tailwind:

We need to require Tailwind as a dev dependency:

yarn add tailwindcss --dev

2. Initialize Tailwind

This generates a tailwind.js config file in styles directory:

./node_modules/.bin/tailwind init resources/assets/styles/tailwind.js

or if you have npx installed globally :raised_hands: (tip: you do have it if you are running npm 5.2.0+):

npx tailwind init resources/assets/styles/tailwind.js

3. Add Tailwind to the top of the PostCSS plugin list

In resources/assets/build/postcss.config.js add:

// ...
plugins: {
  tailwindcss: `${options.paths.assets}/styles/tailwind.js`,
  // ...
},
See entire modified file
/* eslint-disable */

const cssnanoconfig = {
  preset: ['default', { discardcomments: { removeall: true } }]
};

module.exports = ({ file, options }) => {
  return {
    parser: options.enabled.optimize ? 'postcss-safe-parser' : undefined,
    plugins: {
      tailwindcss: `${options.paths.assets}/styles/tailwind.js`, 
      cssnano: options.enabled.optimize ? cssnanoconfig : false,
      autoprefixer: true,
    },
  };
};

4. Add Tailwind directives to Stylelint ignoreAtRules list in package.json:

"ignoreAtRules": [
  // ...
  "tailwind",
  "apply",
  "responsive",
  "variants",
  "screen"
]
See entire modified file
{
  "name": "sage",
  "version": "9.0.0",
  "author": "Roots <team@roots.io>",
  "homepage": "https://roots.io/sage/",
  "private": true,
  "repository": {
    "type": "git",
    "url": "git://github.com/roots/sage.git"
  },
  "bugs": {
    "url": "https://github.com/roots/sage/issues"
  },
  "licenses": [
    {
      "type": "MIT",
      "url": "http://opensource.org/licenses/MIT"
    }
  ],
  "browserslist": [
    "last 2 versions",
    "android 4",
    "opera 12"
  ],
  "stylelint": {
    "extends": "stylelint-config-standard",
    "rules": {
      "no-empty-source": null,
      "at-rule-no-unknown": [
        true,
        {
          "ignoreAtRules": [
            "extend",
            "at-root",
            "debug",
            "warn",
            "error",
            "if",
            "else",
            "for",
            "each",
            "while",
            "mixin",
            "include",
            "content",
            "return",
            "function",
            "tailwind",
            "apply",
            "responsive",
            "variants",
            "screen"
          ]
        }
      ]
    }
  },
  "scripts": {
    "build": "webpack --progress --config resources/assets/build/webpack.config.js",
    "build:production": "webpack --progress -p --config resources/assets/build/webpack.config.js",
    "build:profile": "webpack --progress --profile --json --config resources/assets/build/webpack.config.js",
    "start": "webpack --hide-modules --watch --config resources/assets/build/webpack.config.js",
    "rmdist": "rimraf dist",
    "lint": "npm run -s lint:scripts && npm run -s lint:styles",
    "lint:scripts": "eslint resources/assets/scripts resources/assets/build",
    "lint:styles": "stylelint \"resources/assets/styles/**/*.{css,sass,scss,sss,less}\"",
    "test": "npm run -s lint"
  },
  "engines": {
    "node": ">= 6.9.4"
  },
  "devDependencies": {
    "autoprefixer": "~7.2.5",
    "browser-sync": "~2.23.6",
    "browsersync-webpack-plugin": "^0.6.0",
    "bs-html-injector": "~3.0",
    "buble-loader": "^0.4.1",
    "cache-loader": "~1.2.0",
    "clean-webpack-plugin": "^0.1.18",
    "copy-globs-webpack-plugin": "^0.2.0",
    "css-loader": "^0.28.9",
    "cssnano": "~v4.0.0-rc.2",
    "eslint": "~4.17.0",
    "eslint-loader": "~1.9",
    "eslint-plugin-import": "~2.8.0",
    "extract-text-webpack-plugin": "~3.0.2",
    "file-loader": "^1.1.6",
    "friendly-errors-webpack-plugin": "^1.6.1",
    "imagemin-mozjpeg": "~7.0.0",
    "imagemin-webpack-plugin": "~2.0.0",
    "import-glob": "~1.5",
    "node-sass": "~4.7.2",
    "postcss-loader": "~2.1.0",
    "postcss-safe-parser": "~3.0",
    "resolve-url-loader": "~2.2.1",
    "rimraf": "~2.6",
    "sass-loader": "~6.0",
    "style-loader": "^0.20.1",
    "stylelint": "^8.4.0",
    "stylelint-config-standard": "~18.0.0",
    "stylelint-webpack-plugin": "^0.10.1",
    "tailwindcss": "^0.5.2",
    "url-loader": "^0.6.2",
    "webpack": "~3.10.0",
    "webpack-assets-manifest": "^1.0.0",
    "webpack-dev-middleware": "~2.0.4",
    "webpack-hot-middleware": "~2.21.0",
    "webpack-merge": "~4.1.1",
    "yargs": "~11.0.0"
  },
  "dependencies": {
    "bootstrap": "v4.0.0",
    "jquery": "^3.3.1",
    "popper.js": "^1.12.9"
  }
}

5. Add directives to main.scss:

/** Tailwind base styles – includes modified normalize.css */
@tailwind preflight;

/** Tailwind plugins – included in tailwind.js */
@tailwind components;

/**
 * Sage’s styles
 *
 * Placing these here allows you to use @apply
 * to apply Tailwind’s utilities
 */

/** Tailwind utilities – generated from config in tailwind.js */
@tailwind utilities;
See example

This example is a pure combination of Sage’s default main.scss and Tailwind’s recommended CSS structure:

@import "common/variables";
/**
 * This injects Tailwind's base styles, which is a combination of
 * Normalize.css and some additional base styles.
 *
 * You can see the styles here:
 * https://github.com/tailwindcss/tailwindcss/blob/master/css/preflight.css
 *
 * If using `postcss-import`, you should import this line from it's own file:
 *
 * @import "./tailwind-preflight.css";
 *
 * See: https://github.com/tailwindcss/tailwindcss/issues/53#issuecomment-341413622
 */
@tailwind preflight;

/** Import everything from autoload */
@import "./autoload/**/*";

/**
 * Import npm dependencies
 *
 * Prefix your imports with `~` to grab from node_modules/
 * @see https://github.com/webpack-contrib/sass-loader#imports
 */
// @import "~some-node-module";

/**
 * This injects any component classes registered by plugins.
 *
 * If using `postcss-import`, use this import instead:
 *
 * @import "tailwindcss/components";
 */
@tailwind components;

/**
 * Here you would add any of your custom component classes; stuff that you'd
 * want loaded *before* the utilities so that the utilities could still
 * override them.
 *
 * Example:
 *
 * .btn { ... }
 * .form-input { ... }
 *
 * Or if using a preprocessor or `postcss-import`:
 *
 * @import "components/buttons";
 * @import "components/forms";
 */
@import "common/global";
@import "components/buttons";
@import "components/comments";
@import "components/forms";
@import "components/wp-classes";
@import "layouts/header";
@import "layouts/sidebar";
@import "layouts/footer";
@import "layouts/pages";
@import "layouts/posts";
@import "layouts/tinymce";

/**
 * This injects all of Tailwind's utility classes, generated based on your
 * config file.
 *
 * If using `postcss-import`, you should import this line from it's own file:
 *
 * @import "./tailwind-utilities.css";
 *
 * See: https://github.com/tailwindcss/tailwindcss/issues/53#issuecomment-341413622
 */
@tailwind utilities;

/**
 * Here you would add any custom utilities you need that don't come out of the
 * box with Tailwind.
 *
 * Example :
 *
 * .bg-pattern-graph-paper { ... }
 * .skew-45 { ... }
 *
 * Or if using a preprocessor or `postcss-import`:
 *
 * @import "utilities/background-patterns";
 * @import "utilities/skew-transforms";
 */
A simpler setup
/** Tailwind base styles */
@tailwind preflight;

/** Import everything from autoload */
@import "./autoload/**/*";

/** Tailwind plugins */
@tailwind components;

/** Your own components – checkout @apply */

/** Tailwind utilities */
@tailwind utilities;

/** Define custom utilities */

Check out the Tailwind docs for notes on CSS structure.

6. Add Purgecss for unused CSS removal (optional)

  1. Add Purgecss to Sage.
  2. Once you’ve successfully added Purgecss, you will need to complete an addition step to make sure Purgecss can extract Tailwind’s classes properly. Luckily, Tailwind has a guide in their docs to add a custom Purgecss extractor.
See example

In webpack.config.optimize.js:

'use strict'; // eslint-disable-line

const { default: ImageminPlugin } = require('imagemin-webpack-plugin');
const imageminMozjpeg = require('imagemin-mozjpeg');
const glob = require('glob-all');
const PurgecssPlugin = require('purgecss-webpack-plugin');

const config = require('./config');

class TailwindExtractor {
  static extract(content) {
    return content.match(/[A-z0-9-:\/]+/g) || [];
  }
}

module.exports = {
  plugins: [
    new ImageminPlugin({
      optipng: { optimizationLevel: 7 },
      gifsicle: { optimizationLevel: 3 },
      pngquant: { quality: '65-90', speed: 4 },
      svgo: { removeUnknownsAndDefaults: false, cleanupIDs: false },
      plugins: [imageminMozjpeg({ quality: 75 })],
      disable: (config.enabled.watcher),
    }),
    new PurgecssPlugin({
      paths: glob.sync([
        'app/**/*.php',
        'resources/views/**/*.php',
        'resources/assets/scripts/**/*.js',
      ]),
      extractors: [
        {
          extractor: TailwindExtractor,
          extensions: ["js", "php"]
        }
      ],
    }),
  ],
};

And there you go — you’re all setup. Just run yarn start,1 pop open tailwind.js to define your design system, and start building! :tada:


1 You will notice a Webpack warning when running yarn start. There is nothing going wrong with the compiling of your styles, but this is a known issue with the resolve url Webpack loader. If you comment it out in the Webpack config file, the error will no longer appear, however, an error will persist in the browser console since this breaks source maps. Since you are using Tailwind — which generates CSS via a JavaScript config file — you don’t really need source maps, so you can go even further and remove the sourceMap option form the css/postcss/sass loaders. It does suck, but using a utility CSS framework you’ll notice you will be spending a lot less time in your stylesheets and more time in the markup designing.

:woman_shrugging:

12 Likes

This is awesome, thank you! I’ve been using a custom forked version of Sage 9 beta before the postcss build file was added, so my version was a bit hacky. This is a great guide.

1 Like

Thank you @knowler! That is really useful and the first steps with Tailwind look promising.
However using Tailwind within PHPStorm shows stylint errors for nearly all related functions such as “@apply”, “@tailwind”.
Compiling works fine, but I really hate the red error notices. Sadly wasn’t able to find any resources for how I can Tailwind support in PHPStorm. Do you (or anyone else) have some suggestion how to configute PHPStorm to support Tailwind syntax?

Best regards,

Philipp

1 Like

Hey @philipp - did you already try modifying your Stylelint rules in package.json as described above?

For me (using VS Code), I believe that that was all I needed to do.

1 Like

Hi @mmirus, yes I did. And I even tried VS Code which shows the same errors. Is there any command after modification to run to compile the settings? Otherwise no idea why this works for you. :slight_smile:

For the moment I’m just adding //noinspection CssInvalidAtRuleto all .scss files where I use Tailwind. But that isn’t the solution…

Thank you!

Can you post the output of some of the errors?

“Error” might be the wrong term but I guess the image is self-explaining. It’s about the point that the IDE recognizes the usage as errors.

image

Is this an extension you are using for PHPStorm + Stylelint? If not, would Stylelint have anything to do with the issue? Or would this just be a syntax issue in the IDE?

@knowler you’re right. It has nothing to do with Stylint. It’s simply the IDE own “Syntax Inspection”.

grafik

However I just disabled the “unknown at-rule” check of PHPStorm for the project as Stylint will also let me know if something isn’t valid here. I guess that will be fine for now.

Thank you!

1 Like

I know a fresh Sage install has Tailwind as an option but this guide needs to be updated.

@tailwind preflight has been changed to @tailwind base

:smiley:

2 Likes

The sage-installer that Sage uses isn’t the version with 1.0.0 yet anyway. It hasn’t been updated since February but the Tailwinds 1.0.0 release for the installer was in March. For now you basically have to manually upgrade.

Sage Installer 1.6.3 should include support for Tailwind CSS 1.0:

Just run composer update roots/sage-installer, then manually run the preset CLI:

php ./vendor/bin/sage preset
5 Likes

Thanks didn’t know you could do it that way. Worked great for a new project and live reloading works and everything (unlike my manually upgraded garbage version).

1 Like