Adding Bootstrap 4 and PurgeCSS to Phoenix 1.5

I hear Tailwind is popular now, but I personally do not see any benefit. It just adds extra complexity. First I tried to get used to it but I feel a lot more productive using Bootstrap and I like simple Bootstrap styles better. So I decided to stick with Bootstrap. For setting up Tailwind, this Pragmatic Studio post was helpful, by the way.

日本語版

Here is how I set it up.

1. Install Bootstrap

cd path/to/my/phoenix/app
npm install --prefix assets --save-dev bootstrap@4 purgecss-webpack-plugin glob-all

2. Import Bootstrap CSS

Next we need to import Bootstrap's CSS into our assets/css/app.scss file.

  /* This file is for your main application css. */
  @import "../node_modules/nprogress/nprogress.css";
+ @import "../node_modules/bootstrap/scss/bootstrap.scss";

3. Configure Purge CSS

This is an optional step but we might as well get rid of unused CSS from Bootstrap since it is easy to set up. Open the assets/webpack.config.js file and modify a few locations:

  • Use glob-all instead of default glob because we need to specify multiple filenames to Purge CSS.
  • Require purgecss-webpack-plugin
  • Specify array of file paths that may reference any Bootstrap CSS class by name, which in Phoenix are all the view modules, template files, and JavaScript files.
  const path = require('path');
- const glob = require('glob');
+ const glob = require('glob-all');
  const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
  const MiniCssExtractPlugin = require('mini-css-extract-plugin');
  const TerserPlugin = require('terser-webpack-plugin');
  const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
  const CopyWebpackPlugin = require('copy-webpack-plugin');
+ const PurgecssPlugin = require('purgecss-webpack-plugin');

  module.exports = (env, options) => {
    const devMode = options.mode !== 'production';

    return {

      # ...

      plugins: [
        new MiniCssExtractPlugin({ filename: '../css/app.css' }),
        new CopyWebpackPlugin([{ from: 'static/', to: '../' }]),
+       new PurgecssPlugin({
+         paths: glob.sync([
+           '../lib/**/*.ex',
+           '../lib/**/*.leex',
+           '../lib/**/*.eex',
+           './js/**/*.js',
+           './node_modules/some_library/**/*.js',
+         ]),
+       }),
      ]
      .concat(devMode ? [new HardSourceWebpackPlugin()] : [])
    }
  };

Or if we want to use Purge CSS only in production mode

module.exports = (env, options) => {
  const devMode = options.mode !== 'production';

  return {

    # ...

    plugins: [
      new MiniCssExtractPlugin({ filename: '../css/app.css' }),
      new CopyWebpackPlugin([{ from: 'static/', to: '../' }]),
    ].concat(
      devMode
        ? [
            // development only
            new HardSourceWebpackPlugin(),
          ]
        : [
            // production only
            new PurgecssPlugin({
              paths: glob.sync([
                '../lib/**/*.ex',
                '../lib/**/*.leex',
                '../lib/**/*.eex',
                './js/**/*.js',
                './node_modules/some_library/**/*.js',
              ]),
            }),
          ]
    ),
  }
};

I personally prefer to apply it to both development and production so that I can prevent surprise at deployment.

4. Remove unnecessay Phoenix default CSS

Now that we use Bootstrap CSS, we can remove Phoenix default CSS for the flash message, other than .alert:empty, which hides the flash message when there is nothing to show.

- .alert {
-   padding: 15px;
-   margin-bottom: 20px;
-   border: 1px solid transparent;
-   border-radius: 4px;
- }
- .alert-info {
-   color: #31708f;
-   background-color: #d9edf7;
-   border-color: #bce8f1;
- }
- .alert-warning {
-   color: #8a6d3b;
-   background-color: #fcf8e3;
-   border-color: #faebcc;
- }
- .alert-danger {
-   color: #a94442;
-   background-color: #f2dede;
-   border-color: #ebccd1;
- }
- .alert p {
-   margin-bottom: 0;
- }
  .alert:empty {
    display: none;
  }

5. Override Bootstrap variables (optional)

Optionally, we can override Bootstrap variables so that we can customize the styling. You can find the full list of Bootstrap variables in the library's scss/_variables.scss file.

I personally make a file at assets/css/_variables.scss of my Phoenix project and copy some sections of Bootstrap variables that I intend to override. It may look like below.

@import "../node_modules/bootstrap/scss/functions";

// Custom Variables
// Override Bootstrap variables here.
// https://github.com/twbs/bootstrap/blob/v4-dev/scss/_variables.scss

// Color system

$white:    #fff;
$gray-100: #eceff1;
$gray-200: #cfd8dc;
$gray-300: #b0bec5;
$gray-400: #90a4ae;
$gray-500: #78909c;
$gray-600: #607d8b;
$gray-700: #546e7a;
$gray-800: #455a64;
$gray-900: #37474f;
$black:    #263238;

$grays: ();
// stylelint-disable-next-line scss/dollar-variable-default
$grays: map-merge(
  (
    "100": $gray-100,
    "200": $gray-200,
    "300": $gray-300,
    "400": $gray-400,
    "500": $gray-500,
    "600": $gray-600,
    "700": $gray-700,
    "800": $gray-800,
    "900": $gray-900
  ),
  $grays
);

// https://material.io/resources/color
$blue:    #2962FF;
$indigo:  #304FFE;
$purple:  #AA00FF;
$pink:    #C51162;
$red:     #D50000;
$orange:  #FF6D00;
$yellow:  #FFD600;
$green:   #00C853;
$teal:    #00BFA5;
$cyan:    #00B8D4;

One important thing is we should remove !default when we copy Bootstrap variable from the library's file because our custom values will be final, not default.

Then I import my custom variables in my assets/css/app.scss file.

  /* This file is for your main application css. */
+ @import 'variables';
  @import '../node_modules/nprogress/nprogress.css';
  @import '../node_modules/bootstrap/scss/bootstrap.scss';

6. Add custom CSS (optional)

  /* This file is for your main application css. */
  @import 'variables';
  @import '../node_modules/nprogress/nprogress.css';
  @import '../node_modules/bootstrap/scss/bootstrap.scss';
+ @import 'my_custom_syles';

That's it!