Skip to content

Instantly share code, notes, and snippets.

@josephan
Last active August 8, 2023 05:50
  • Star 49 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save josephan/45569c48ee4867237e89417aed283103 to your computer and use it in GitHub Desktop.
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 */
@joshuataylor
Copy link

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

@josephan
Copy link
Author

@joshuataylor Thanks, updated.

@zbarnes757
Copy link

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
Copy link

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
Copy link
Author

@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
Copy link
Author

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

@mingfang
Copy link

@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
Copy link

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