Adding Bootstrap 4 and PurgeCSS to Phoenix 1.5
Tags: tailwindcss, bootstrap, elixir, phoenix
Published: 2021-02-14 22:10:45

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](https://pragmaticstudio.com/tutorials/adding-tailwind-css-to-phoenix) was helpful, by the way.

[日本語版](https://qiita.com/mnishiguchi/items/5118e2fadd7780a4702d)

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.

```diff
  /* 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`](https://www.npmjs.com/package/glob-all) instead of default `glob` because we need to specify multiple filenames to Purge CSS.
- Require [purgecss-webpack-plugin](https://www.npmjs.com/package/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.

```diff
  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

```js
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.

```diff
- .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`](https://github.com/twbs/bootstrap/blob/v4-dev/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.

```scss
@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`](https://sass-lang.com/documentation/variables#default-values) 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.

```diff
  /* 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)

```diff
  /* 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!