Webpack Configuration for React and TailwindCSS

February 5th 2020

When it comes to utility first css libraries, tailwindcss is one of the front runners in css frameworks. Tailwind is summed up nicely in a couple sentences:

Tailwind CSS is a highly customizable, low-level CSS framework that gives you all of the building blocks you need to build bespoke designs without any annoying opinionated styles you have to fight to override.

However, it’s different than a lot of frameworks such as Bootstrap, Bulma and Zurb Foundation in the fact that it is not a Sass framework. Instead, it’s a PostCSS plugin and thus requires PostCSS installation and configuration.

Likewise, the go to for React development is the create-react-app (CRA) npx project that the React team has built. Although CRA comes with a lot of nice dev and build tools out of the box, it doesn’t allow webpack configuration (without some lengthy eject configuration). Furthermore, it supports Sass out of the box but not PostCSS plugins, so in order to use Tailwind in a CRA you would have to eject your webpack config or use a project such as react-app-rewired to allow for more granular webpack config.

Fortunately, configuring a webpack project with React and Tailwind from scratch is not too difficult, and I’m going to show you how to do that in this tutorial!


1. Step One (the setup)

We’ll have to setup a few prerequisits and install a few packages before we get going. Let’s first create a folder mkdir react-tailwind-tutorial && cd react-tailwind-tutorial. Then we can execute a simple npm init -y command to create a new npm package. This creates a package.json file for us with some default configurations so we can start installing our dependencies. Here are the dependencies we will need:

  • @babel/core
  • @babel/preset-env
  • @babel/preset-react
  • babel-loader
  • css-loader
  • mini-css-extract-plugin
  • postcss-loader
  • react
  • react-dom
  • tailwindcss
  • webpack
  • webpack-cli

Ufta, that’s a lot of dependencies, but have no fear, we will break all of them down for you later. Let’s install them:
npm i --save @babel/core @babel/preset-env @babel/preset-react babel-loader css-loader mini-css-extract-plugin postcss-loader react react-dom tailwindcss webpack webpack-cli

…and we wait for installation.
…still waiting.
…finally everything is installed!


2. Step Two (the Webpack config)

With all of our dependencies installed we can dive into the fun part, Webpack config**!
**Note** there are many ways to configure your Webpack, thus a lot of different setups and configurations that could work. This tutorial is how I’ve successfully configured Tailwind in a React app for a few different projects.

Let’s start by creating a simple webpack.config.js file: touch webpack.config.js

You can open this in your editor of choice, but recommend one that supports .js files such as sublime, visual studio code or straight up vim editor.

Let’s add some bare minimum configuration:

const path = require('path')
module.exports = (env, argv) => {
  return {
    entry: path.join(__dirname, 'src', 'index.js'),
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'bundle.js'
    }
  }
}

Alright, pretty simple so far, let’s explain for those who aren’t familiar with webpack config. This exports a configuration object that the webpack cli (or dev server which we’ll setup later) expects. In the configuration object we’re specifying and entry point: entry: path.join(__dirname, 'src', 'index.js') using the node path module. What this says is create a path by joining the current directory we’re in plus a “src” directory plus a file named “index.js”. On a mac computer this path might look like this: /Users/tthenne/www/react-tailwind-tutorial/src/index.js. This is where we will “bootstrap” our react app and be the entry point for our project. Likewise the output property states:

    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'bundle.js'
    }

Which uses path.resolve and resolves a path similarily to path.join by using __dirname and appending “dist” folder. If you logged this it would look similar (minus the filename) /Users/tthenne/www/react-tailwind-tutorial/src/index.js. This is our export path that webpack will write to, specifically a file named “bundle.js” in our output folder.

Before going too far let’s create some of the folder/file structure we defined in our webpack config:
mkdir dist && mkdir src && touch dist/bundle.js && touch src/index.js

We will also need an “index.html” file that the dev server (or web server) can serve:
touch dist/index.html

And we can create a quick scaffold:

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>React Redux</title>
  <link rel="stylesheet" type="text/css" href="/main.bundle.css">
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>

<body>
  <div id="app"></div>
  <script src="/bundle.js" async defer></script>
</body>

</html>

Hopefully nothing too unfamiliar here. One thing to node is that we’re creating a script tag that is including our webpack export, bundle file: <script src="/bundle.js" async defer></script> so that will serve our js code (namely our react app).

Alright, good job. Take a breather, maybe a quick walk or refill that coffee cup! Next we’ll dive into our webpack rules for some more configuration!


3. Step Three (webpack config continued…babel & react)

Since we’re building a react app we’ll have to do some babel configuration to “transform” our javascript into javascript modern browsers can understand and interpret. Babel is a javascript compiler that does a lot of heavy lifting for us. First, we’ll need to define a webpack “rule” that looks for javascript files and uses babel to compile them. In our webpack config we’ll add under our outputs property:

const path = require('path')
module.exports = (env, argv) => {
  return {
    entry: path.join(__dirname, 'src', 'index.js'),
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'bundle.js'
    },
    module: {
      rules: [
        {
          test: /\.(jsx|js)$/,
          include: path.resolve(__dirname, 'src'),
          exclude: /node_modules/,
          use: [{
            loader: 'babel-loader',
            options: {
              presets: [
                ['@babel/preset-env', {
                  "targets": {
                    "node": "12"
                  }
                }],
                '@babel/preset-react'
              ]
            }
          }]
        },
      ]
    },
  }
}

The rules property in webpack is an array of objects, each object specifying options of what webpack should do when it finds a file via our “test” property. The “test” property can be a regular expression. In our case we want to test if files end in “.jsx” or “.js” – which react components do. Also, note that we are only “looking” for (or including) files in our “src” directory (which we created earlier) and excluding our /node_modules/ directory. The exclude property is important so webpack doesn’t compile everything within our node_modules folder (including our dependencies we installed). Once it finds a file of type “.jsx” or “.js” it will use the babel-loader package (we installed earlier) and that babel-loader takes a few options. Let’s walk through those. The “presets” option basically tells babel the preset (or in our case the environment – env) to target so that the compiled code would compile to the standards of that environment. We’re using the node version 12 target to compile our code to that versions standards. Secondly, @babel/preset-react handles all of the “react specific” such as JSX syntax and other react specific needs. The big takeaway here is: find files of type “.js” or “.jsx” and using babel compile them to the babel presets we defined!

Now that we have our javascript files taken care of we can move on to our css (and most importantly) our PostCSS.


4. Step Four (webpack config continued…css, miniCssExtractPlugin, css-loader and PostCss)

The CSS, what we came here for. Let’s jump in with miniCssExtractPlugin. As per the docs: This plugin extracts CSS into separate files. It creates a CSS file per JS file which contains CSS. We’ll use this to create our “main.bundle.css” file which will be included in our index.html file as our main css that every other css gets loaded into (including tailwind library). At the top of our webpack.config.js file we’ll need to include:

const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = (env, argv) => {
  return {
    ...
  }
}

Then we’ll have to add a new property called “plugins” to our module.exports return object like so:

const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = (env, argv) => {
  return {
    entry: path.join(__dirname, 'src', 'index.js'),
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'bundle.js'
    },
    plugins: [
      new MiniCssExtractPlugin({
        filename: '[name].bundle.css',
        chunkFilename: '[id].css'
      }),
    ],
    ...
  }
}

As you can see the miniCssExtract plugin will create a file called: [name].bundle.css and will be written (by default without other configuration options) to our output webpack path: path: path.resolve(__dirname, 'dist')

Next we’ll have to create another rule that looks for “.css” files and handles those files with the loaders that we installed earlier:

const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = (env, argv) => {
  return {
    entry: path.join(__dirname, 'src', 'index.js'),
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'bundle.js'
    },
    plugins: [
      new MiniCssExtractPlugin({
        filename: '[name].bundle.css',
        chunkFilename: '[id].css'
      }),
    ],
    module: {
      rules: [
        {
          test: /\.(jsx|js)$/,
          include: path.resolve(__dirname, 'src'),
          exclude: /node_modules/,
          use: [{
            loader: 'babel-loader',
            options: {
              presets: [
                ['@babel/preset-env', {
                  "targets": {
                    "node": "12"
                  }
                }],
                '@babel/preset-react'
              ]
            }
          }]
        },
        {
          test: /\.css$/i,
          include: path.resolve(__dirname, 'src'),
          exclude: /node_modules/,
          use: [
            'style-loader',
            {
              loader: MiniCssExtractPlugin.loader,
              options: { 
                hmr: argv.mode === 'development' 
              }
            },
            {
              loader: 'css-loader',
              options: {
                importLoaders: 1
              }
            },
            'postcss-loader'
          ]
        },
      ]
    },
  }
}

There’s quite a bit going on here. Let’s walk through it. We’re looking for “.css” files and only in our “src” folder of our project, making sure we’re excluding /node_modules/ folder. Then we’re using ‘style-loader’, MiniCssExtractPlugin.loader, ‘css-loader’, and finally ‘postcss-loader’. One option in our MiniCssExtractPlugin.loader is the ‘hmr’ option if we pass a --mode development argument to our webpack build (or webpack dev server) command which we’ll setup later and forces the main.bundle.css to be rebuilt and served on any “.css” file change without reloading our browser. How cool (and time saving) is that? Speaking of webpack dev server, let’s set that up so our development process is smooth and painless. You’ll really enjoy this option for development.


5. Step Five (webpack config continued…webpack dev server)

In production deploy we will more than likely run a webpack build command but for development we’ll need a server to watch for file changes and server our index.html file with the aforementioned changes. Luckily, webpack comes with a plugin called the HotModuleReplacementPlugin which does exactly what we’re looking for. All we need to do is setup a few more configuration options in our webpack config file to enable hot module reloading (HMR). Let’s take a look at what that might look like:

const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { HotModuleReplacementPlugin } = require('webpack')

module.exports = (env, argv) => {
  return {
    entry: path.join(__dirname, 'src', 'index.js'),
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'bundle.js'
    },
    plugins: [
      new MiniCssExtractPlugin({
        filename: '[name].bundle.css',
        chunkFilename: '[id].css'
      }),
      new HotModuleReplacementPlugin(),
    ],
    devServer: {
      open: true,
      clientLogLevel: 'silent',
      contentBase: './dist',
      historyApiFallback: true,
      hot: true,
    },
    module: {
      rules: [
        {
          test: /\.(jsx|js)$/,
          include: path.resolve(__dirname, 'src'),
          exclude: /node_modules/,
          use: [{
            loader: 'babel-loader',
            options: {
              presets: [
                ['@babel/preset-env', {
                  "targets": {
                    "node": "12"
                  }
                }],
                '@babel/preset-react'
              ]
            }
          }]
        },
        {
          test: /\.css$/i,
          include: path.resolve(__dirname, 'src'),
          exclude: /node_modules/,
          use: [
            'style-loader',
            {
              loader: MiniCssExtractPlugin.loader,
              options: { 
                hmr: argv.mode === 'development' 
              }
            },
            {
              loader: 'css-loader',
              options: {
                importLoaders: 1
              }
            },
            'postcss-loader'
          ]
        },
      ]
    },
  }
}

And ta-da we have a development server setup with HMR working. Let’s walk through quick what we did. We required the HotModuleReplacementPlugin from webpack package by restructuring the class from webpack. We then added a new instance of HotModuleReplacementPlugin to our plugins. Finally, we added the property to our exports.module called “devServer” with some configuration options. Also, since we already have our miniCssExtractPlugin set to use hmr (in development mode) our server will reload any css that it detects changes on. Let’s do one more thing. We’ll add a command in our package.json file so we can start our dev server. In package.json under scripts lets add

scripts": {
  "dev": "webpack-dev-server --mode development",
},

This way when we’re ready we can run npm run dev and our webpack-dev-server will be started…and we are almost ready just one more thing, Tailwind!


6. Step Six (PostCSS and TailwindCSS config)

What we finally came here for! Getting tailwindcss working with react has been a bit of a journey but it’s all about to pay off. Let’s dive in. Since we have postcss-loader configured in webpack it expects a postcss.config.js file in our root directory. Let’s create one touch postcss.config.js and the file will simply look like:

module.exports = {
  plugins: [
    require('tailwindcss')
  ],
};

Since tailwind is a postcss plugin all we have to do is require it in our plugins array for our exports object. If we want to configure our tailwind or have configuration options we can also run this command in the root of our project npx tailwindcss init which creates a basic tailwind.config.js file for us. All done here let’s slide into home plate.


6. Step Seven (Putting it all together)

Our index.js file in our src directory is the entry point to our app. Since it’s a react app we need to render it to the DOM. Let’s edit index.js to reflect that:

import React from 'react';
import { render } from 'react-dom';

render(<h1>Hello React Tailwind</h1>, document.getElementById('app'))

This also happens to be the entry point for our css file and since tailwind is a postcss plugin it will look through our css for tailwind directives to add the library to. Let’s create a file in our “src” folder called index.css touch index.css that looks like:

@tailwind base;
@tailwind components;
@tailwind utilities;

And lastly we need to import that into our index.js file since it’s our entry point:

import React from 'react';
import { render } from 'react-dom';
import './index.css';

render(<h1>Hello React Tailwind</h1>, document.getElementById('app'))

Wow we are there! All we need to do now is start our dev server from the command line. Make sure you are in the root directory of your project and run npm run dev. Webpack dev server should open a browser window and serve your index.html file after it’s done building! You can inspect the source code from the browser and look at the main.bundle.css and see that tailwind directives have been replaced by the styles the library produces. What a sight to see!


We’ve been through a lot here in this tutorial and it’s probably felt like a marathon but once you’ve done it once it’s easy to go back and reference your previous configurations. The sky is the limit and hope you build something cool to share with the world. Until next time, stay curious, stay creative!