Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Add Tailwind CSS to an Elixir/Phoenix Project with PurgeCSS

Thanks to the original blog post: https://equimper.com/blog/how-to-setup-tailwindcss-in-phoenix-1.4

1. Install tailwindcss and postcss-loader from npm:

cd assets
npm i --save-dev tailwindcss postcss-loader postcss-import

2. Initialize tailwind (still in assets directory)

npx tailwind init

3. Create assets/postcss.config.js file and add contents:

module.exports = {
  plugins: [
    require('postcss-import')(),
    require('tailwindcss')('./tailwind.config.js'),
    require('autoprefixer'),
  ],
};

4. Update assets/webpack.config.js:

// assets/webpack.config.js

const path = require('path');
const glob = require('glob');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = (env, options) => ({
  optimization: {
    minimizer: [
      new UglifyJsPlugin({ cache: true, parallel: true, sourceMap: false }),
      new OptimizeCSSAssetsPlugin({}),
    ],
  },
  entry: {
    './js/app.js': ['./js/app.js'].concat(glob.sync('./vendor/**/*.js')),
  },
  output: {
    filename: 'app.js',
    path: path.resolve(__dirname, '../priv/static/js'),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.css$/,
-         use: [MiniCssExtractPlugin.loader, 'css-loader']
+         use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({ filename: '../css/app.css' }),
    new CopyWebpackPlugin([{ from: 'static/', to: '../' }]),
  ],
});

5. Override your assets/css/app.css:

/** assets/css/app.css */

/**
 * This injects Tailwind's base styles, which is a combination of
 * Normalize.css and some additional base styles.
 *
 * If using `postcss-import`, use this import instead:
 *
 * @import "tailwindcss/base";
 */
 @tailwind base;

 /**
  * 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";
  */

 /**
  * This injects all of Tailwind's utility classes, generated based on your
  * config file.
  *
  * If using `postcss-import`, use this import instead:
  *
  * @import "tailwindcss/utilities";
  */
 @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";
  */

6. PurgeCSS

With PurgeCSS you can reduce your CSS asset from approx ~300KB to under 10KB. This will improve the performance of your site.

cd assets
npm i -D purgecss-webpack-plugin glob-all

Then in your webpack.config.js

// assets/webpack.config.js

const path = require('path');
const glob = require('glob');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
+ const PurgecssPlugin = require('purgecss-webpack-plugin');
+ const globAll = require('glob-all');

+ // Custom PurgeCSS extractor for Tailwind that allows special characters in
+ // class names.
+ // Regex explanation: https://tailwindcss.com/docs/controlling-file-size/#understanding-the-regex
+ const TailwindExtractor = content => {
+   return content.match(/[\w-/:]+(?<!:)/g) || [];
+ };

module.exports = (env, options) => ({
  optimization: {
    minimizer: [
      new UglifyJsPlugin({ cache: true, parallel: true, sourceMap: false }),
      new OptimizeCSSAssetsPlugin({}),
+        new PurgecssPlugin({
+          paths: globAll.sync([
+            '../lib/<APP_NAME>_web/templates/**/*.html.eex',
+            '../lib/<APP_NAME>_web/views/**/*.ex',
+            '../assets/js/**/*.js',
+          ]),
+          extractors: [
+            {
+              extractor: TailwindExtractor,
+              extensions: ['html', 'js', 'eex', 'ex'],
+            },
+          ],
+        }),
    ],
  },
  entry: {
    './js/app.js': ['./js/app.js'].concat(glob.sync('./vendor/**/*.js')),
  },
  output: {
    filename: 'app.js',
    path: path.resolve(__dirname, '../priv/static/js'),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({ filename: '../css/app.css' }),
    new CopyWebpackPlugin([{ from: 'static/', to: '../' }]),
  ],
});

Note: PurgeCSS will remove css classes you wrote to style HTML that comes from outside your code base (like a hex package, i.e pagination library). In those cases explicitly tell PurgeCSS to not remove them with:

/* purgecss start ignore */

.pagination-class-in-your-css-file {
  background-color: #fff;
}

/* purgecss end ignore */
@carterbryden

This comment has been minimized.

Copy link

@carterbryden carterbryden commented Jun 24, 2019

Awesome! Will this run purgecss before live-reloading on dev when a watched file changes?

@josephan

This comment has been minimized.

Copy link
Owner Author

@josephan josephan commented Jun 24, 2019

@carterbryden No, in development you will have access to all classes. PurgeCSS will only run when you build assets for production.

@praveenperera

This comment has been minimized.

Copy link

@praveenperera praveenperera commented Sep 26, 2019

My postcss.config.js

const purgecss = require("@fullhuman/postcss-purgecss")({
  content: ["../**/*.html.eex", "./js/**/*.js", "../**/*_view.ex"],
  defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || []
});

module.exports = {
  plugins: [
    require("postcss-import"),
    require("tailwindcss"),
    require("autoprefixer"),
    ...(process.env.NODE_ENV === "production" ? [purgecss] : [])
  ]
};

In the content section you need to account for each place where you may a tailwindcss class, or purgecss will remove it in prod.

@dkuku

This comment has been minimized.

Copy link

@dkuku dkuku commented Oct 7, 2019

My postcss.config.js
In the content section you need to account for each place where you may a tailwindcss class, or purgecss will remove it in prod.
@praveenperera
can you post also the webpack config - I'm struggling with that - the file is always over 400kb with only 1 page

@praveenperera

This comment has been minimized.

Copy link

@praveenperera praveenperera commented Oct 8, 2019

can you post also the webpack config - I'm struggling with that - the file is always over 400kb with only 1 page

sure:

const path = require("path");
const glob = require("glob");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");

module.exports = (env, options) => ({
  optimization: {
    minimizer: [
      new UglifyJsPlugin({ cache: true, parallel: true, sourceMap: false }),
      new OptimizeCSSAssetsPlugin({})
    ]
  },
  entry: {
    app: ["./js/app.js"].concat(glob.sync("./vendor/**/*.js")),
    react: ["./js/react.js"]
  },
  output: {
    filename: "[name].js",
    path: path.resolve(__dirname, "../priv/static/js")
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      },
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          { loader: "css-loader", options: { importLoaders: 1 } },
          "postcss-loader"
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({ filename: "../css/app.css" }),
    new CopyWebpackPlugin([{ from: "static/", to: "../" }])
  ]
});

Also make sure when you are building for production that set set NODE_ENV to production.

// package.json
    "deploy": "NODE_ENV=production webpack --mode production"
@dkuku

This comment has been minimized.

Copy link

@dkuku dkuku commented Oct 8, 2019

@ChangJoo-Park

This comment has been minimized.

Copy link

@ChangJoo-Park ChangJoo-Park commented Oct 10, 2019

@tailwind preflight; need change to @tailwind base;

@josephan

This comment has been minimized.

Copy link
Owner Author

@josephan josephan commented Oct 10, 2019

@ChangJoo-Park thanks updated

@Awlexus

This comment has been minimized.

Copy link

@Awlexus Awlexus commented Oct 21, 2019

Hello, when I ran step 2 it didn't create a file called tailwind.js, but one called tailwind.config.js. I had to adjust that in step 3. Is that a recent change?

@josephan

This comment has been minimized.

Copy link
Owner Author

@josephan josephan commented Oct 21, 2019

@Awlexus Yes, tailwind.config.js is the right file name. Updated the instructions. Thanks!

@josephan

This comment has been minimized.

Copy link
Owner Author

@josephan josephan commented Nov 10, 2019

I updated my PurgeCSS configs in webpack.config.js to not purge CSS classes in my _view.ex files and JavaScript files.
Here is the revision: https://gist.github.com/josephan/45569c48ee4867237e89417aed283103/revisions#diff-22b1984e9055744bcb6b52260dfdfb71

@joshuataylor

This comment has been minimized.

Copy link

@joshuataylor joshuataylor commented Nov 13, 2019

I also had to install postcss-import with yarn/npm.

@josephan

This comment has been minimized.

Copy link
Owner Author

@josephan josephan commented Nov 21, 2019

@joshuataylor Thanks, updated.

@zbarnes757

This comment has been minimized.

Copy link

@zbarnes757 zbarnes757 commented Jan 24, 2020

Did anyone else get the following error with the above gist? From everything I'm seeing online, this setup looks correct but for the life of me idk what I'm missing.

TypeError: Class constructor TailwindExtractor cannot be invoked without 'new'
    at extractSelectors (/app/assets/node_modules/purgecss/lib/purgecss.js:1:2420)
    at PurgeCSS.extractSelectorsFromFiles (/app/assets/node_modules/purgecss/lib/purgecss.js:1:5864)
@zbarnes757

This comment has been minimized.

Copy link

@zbarnes757 zbarnes757 commented Jan 24, 2020

Did anyone else get the following error with the above gist? From everything I'm seeing online, this setup looks correct but for the life of me idk what I'm missing.

TypeError: Class constructor TailwindExtractor cannot be invoked without 'new'
    at extractSelectors (/app/assets/node_modules/purgecss/lib/purgecss.js:1:2420)
    at PurgeCSS.extractSelectorsFromFiles (/app/assets/node_modules/purgecss/lib/purgecss.js:1:5864)

If anybody else finds this, the new version of Purgecss is just expecting a function instead of a class.

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

becomes

const TailwindExtractor = content => {
  return content.match(/[A-Za-z0-9-_:\/]+/g) || [];
};
@josephan

This comment has been minimized.

Copy link
Owner Author

@josephan josephan commented Jan 24, 2020

@zbarnes757 Thanks for posting the fix! Updated the gist. At the time of writing purgecss-webpack-plugin was at 1.4 now it looks like it's 2.0 explaining the breaking changes.

@josephan

This comment has been minimized.

Copy link
Owner Author

@josephan josephan commented Jan 24, 2020

I've also updated the regex reflected in Tailwind's docs: https://tailwindcss.com/docs/controlling-file-size/#understanding-the-regex

@mingfang

This comment has been minimized.

Copy link

@mingfang mingfang commented Mar 13, 2020

@josephan
This is a great gist. Thanks.
I use this to create a complete Elixir+Phoenix+Tailwind template here https://github.com/legionx-com/elixir_phoenix_template

@fschoenfeldt

This comment has been minimized.

Copy link

@fschoenfeldt fschoenfeldt commented Aug 27, 2020

How does this work without providing purgecss in this step? https://gist.github.com/josephan/45569c48ee4867237e89417aed283103#3-create-assetspostcssconfigjs-file-and-add-contents
for me, this gist doesn't really work. I get an error:

warn - Tailwind is not purging unused styles because no template paths have been provided.
warn - If you have manually configured PurgeCSS outside of Tailwind or are deliberately not removing unused styles, set `purge: false` in your Tailwind config file to silence this warning.

In the tailwind docs, they do it differently: https://tailwindcss.com/docs/controlling-file-size#setting-up-purge-css-manually

Edit: Maybe it's because of the order I load my plugins?

          use: [
            MiniCssExtractPlugin.loader,
            "css-loader",
            "postcss-loader",
            "sass-loader",
          ],
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment