Dec 2017

Theming with Webpack

In this tutorial I will show you how to achieve a decent level of theming for your frontend project with the help of webpack and a few webpack plugins. I will not be using any specific framework, just plain JavaScript, HTML, SCSS and webpack.

This tutorial will go through four main steps: setting up a project, setting up webpack to run development server and build the project, theming in SCSS and theming in JavaScript. Feel free to skim through or skip entirely the first two steps, if you have a basic project with some contents and webpack running. Let's get started!

1. Project setup

Let's start by creating the project structure for this demo:

  mkdir theming # or anything else you would like to name your project
  cd theming
  mkdir src # our working directory
  cd src
  mkdir styles # a folder for our styles
  mkdir js # a folder for our JavaScript

We will need to add several dependencies throughout this tutorial, so let's create a package.json file interactively by running the following command from the root of our project:

  yarn init # or npm init if you prefer

Next, we will add some dummy contents in the index.html in the src folder:

  <!DOCTYPE html>
  <html>
    <head>
      <title>Theming</title>
      <meta name="viewport" content="width=device-width, initial-scale=1">
    </head>
    <body>
      <section class="page">
        <h1>Theming demo</h1>

        <section class="page__content">
          <p>
            Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
            tempor incididunt ut labore et dolore magna aliqua.
          </p>
          <p>
            Ut enim ad minim veniam,
            quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
            consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
            cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
            proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
          </p>
          <p>
            Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
            tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
            quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
            consequat.
          </p>
          <p>
            Duis aute irure dolor in reprehenderit in voluptate velit esse
            cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
            proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
          </p>
        </section>
      </section>
    </body>
  </html>

In the styles folder we created earlier, add an index.scss file with some styles:

  @import url('https://fonts.googleapis.com/css?family=Roboto:400,500,700');

  body {
    background-color: #fff;
    font-family: 'Roboto', sans-serif;
    color: $text-color;
    line-height: 1.5;

    transition: all 0.5s;
  }

  .page {
    width: 100%;
    max-width: 1200px;
    margin: 0 auto;
    box-sizing: border-box;
    padding: 0 25px;
  }

  .page__content {
    width: 100%;
    max-width: 800px;
    margin: 0 auto;
  }

Now let's create our main JavaScript file index.js in the js folder and leave it empty for the time being.

We have project structure in place, with some html and css. Styles are not yet included in the html, but don't worry, our future selves will take care of that later.

By now your project folder structure should look like this:

  .
  ├── src
  │   ├── js
  │   │   └── index.js
  │   └── styles
  │       └── index.scss
  ├── index.html
  └── package.json

2. Setup webpack config

First, we need to add webpack and webpack-dev-server as dev dependencies to our project:

  yarn save --dev webpack webpack-dev-server

We would like to use webpack via our npm scripts so next we add the following commands to the scripts section of our package.json like so:

  "scripts": {
    "dev": "webpack-dev-server",
    "build": "webpack`
  }

If we run now yarn run build or yarn run dev, we will get an error:

No configuration file found and no entry configured via CLI option.

In order to make this error go away we will create a webpack.config.js at the root of our project directory and fill it with the necessary configuration:

  // webpack.config.js
  const path = require('path');

  module.exports = {
    entry: './src/js/index.js',
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
    }
  }

The entry tells webpack which file will be the starting point, this is our main JavaScript file and then the output points where to output the processed files and how to name them. You might have noticed we are also importing an npm package called path: it is to help us determine the right place to output our bundle. We will stick to the convention and name our output file with the same name as the input file with the addition of 'bundle' in the name.

Now when running yarn run build we will get a bundle named main.bundle.js in the dist folder. On yarn run start we will get to see our site on localhost:8080. 🎉 But we are not done just yet!

What we need to do next is include the bundled JavaScript in the main HTML file. Now, we don't want to do this manually, so we will use a webpack plugin, html-webpack-plugin that does exactly that. Let's add it to the project dev dependencies:

  yarn add --dev html-webpack-plugin

Next, let's update the webpack configuration so it uses this newly added plugin. At the top of our webpack.config.js import the new plugin:

  // webpack.config.js
  const htmlWebpackPlugin = require('html-webpack-plugin');

Next, we tell webpack to use this new plugin and inject the script tag including our newly created bundle in the file specified in the template option:

  // webpack.config.js
  plugins: [
    new htmlWebpackPlugin ({
      inject: true,
      template: './index.html'
    })
  ]

So far so good! We have webpack running, bundling our JavaScript and injecting the bundle straight into our HTML. Remember our future selves were supposed to include the styles in our HTML? Now is the time, so let's import our main SCSS file in our JavaScript:

  // src/index.js
  // Import main scss file
  import './styles/index.scss';

In order to process SCSS with webpack though, we would need to add 3 loaders:

  yarn save --dev style-loader css-loader sass-loader

Loaders helps us apply transformations to our code. The sass-loader will load a SASS/SCSS file and then compile it to CSS. The css-loader is helpful for resolving imports in the stylesheets. The style-loader will transform any stylesheet into a JavaScript module so it can be included in the bundle.

Let's make use of these loaders in our webpack.config.js by including them right after the output:

  module: {
    rules: [{
      test: /\.scss$/,
      use: [
        {
          loader: "style-loader"
        },
        {
          loader: "css-loader"
        },
        {
          loader: "sass-loader",
        }
      ]
    }]
  },

Here we tell webpack to process each .scss file with these 3 loaders. The output from one loader is the input for the next one, but the important part here is that the order is from right to left. So in this case first sass will be compiled to css by the sass-loader, then all imports will be resolved by the css-loader and the final css will be transformed in a module to be included in our JavaScript bundle by the style-loader.

3. Theming SCSS

Let's create 2 theme files and add some variables into them that will help us change a bit the look and feel of our main page:

  // src/styles/_theme_a.scss

  // Colors
  $brand-color: #d4d4d4;
  $text-color: #333;

  // Typography
  @import url('https://fonts.googleapis.com/css?family=Lora:300,400,700');

  $site-font: 'Lora', serif;
  $headline-font: $site-font;

  // UI Elements
  // Headlines
  h1 {
    font-size: 36px;
    font-weight: 300;
    text-align: center;
  }
  // src/styles/_theme_b.scss

  // Colors
  $brand-color: deepskyblue;
  $text-color: #333;

  // Typography
  @import url('https://fonts.googleapis.com/css?family=Pacifico');
  @import url('https://fonts.googleapis.com/css?family=Roboto:400,500,700');

  $site-font: 'Roboto', sans-serif;
  $headline-font: 'Pacifico', cursive;

  // UI Elements
  // Headlines
  h1 {
    font-family: $headline-font;
    font-size: 42px;
    font-weight: bold;
    letter-spacing: 1.2px;
  }

By now your project structure should be looking like this:

  .
  ├── src
  │   ├── js
  │   │   └── index.js
  │   └── styles
  │       ├── _theme-a.scss
  │       ├── _theme-b.scss
  │       └── index.scss
  ├── index.html
  ├── package.json
  └── webpack.config.js

Next, let's update the styles in our main .scss file. We need to remove the fonts import at the top of the file, as now we are using different fonts, background and text colors for each theme with the variables we just defined:

  body {
    background-color: $brand-color;
    font-family: $site-font;
    color: $text-color;
    line-height: 1.5;
  }

It would be awesome if we could import these theme partials dynamically in our main .scss file, wouldn't it?

In order to achieve this we will make use of the sass loader and its data option.

The data option allows us to prepend SCSS to the entry file. It also allows for the use of environment variables. An environment variable is a variable that if set becomes available in the process we are currently running.

Let's update our webpack.config.js:

  {
    loader: "sass-loader",
    options: {
      data: `@import "${path.resolve(__dirname, `./src/styles/theme-${process.env.THEME}.scss`)}";`
    }
  }

What is happening here is that we tell the sass-loader to grab the styles from the specified file and prepend them to our css file. The name of the file to use depends on an environment variable that we set. Now we need a way to set the environment variable when running the dev server or when buidling the project.

Let's add a new npm script:

  "dev:theme": "THEME=$THEME webpack-dev-server"

Now we can call the script like this:

  THEME='a' yarn run dev:theme

and we can see our styles change based on the theme that was chosen. I like this approach because you can develop UI themes while working in the same repository but our final build will include only the theme we specified. On the downside, source mappings will be broken.

4. Theming JavaScript

Now let's create two JavaScript modules with some theme configuration:

  // src/js/theme-a.js
  const title = 'Hi, theme A here! How do you do?';

  export default {
    title,
  };
  // src/js/theme-b.js
  const title = 'Good day from theme B!';

  export default {
    title,
  };

Currently the JavaScript theming configuration contains just a title, but can potentailly hold any kind of information. For example, different date or number formatting functions, path to assets, components to display, layout config... you name it!

Your project structure should look like this:

  .
  ├── src
  │   ├── js
  │   │   ├── index.js
  │   │   ├── theme-a.js
  │   │   └── theme-b.js
  │   └── styles
  │       ├── _theme-a.scss
  │       ├── _theme-b.scss
  │       └── index.scss
  ├── index.html
  ├── package.json
  └── webpack.config.js

Now that we have two JavaScript theme configurations, we also want to be able to import any of them on demand. We will use the THEME environment variable which we need to make globally accessible in our JavaScript with the define plugin:

  // webpack.config.js

  new webpack.DefinePlugin({
    'THEME': JSON.stringify(process.env.THEME),
  })

ECMAScript modules imports are static, but thanks to webpack we can use dynamic import(). import() accepts a parameter that specifies where to find the module we want to import and returns a promise. The parameter can be any expression that returns a string, so we can use our environment variable to build the path to the theme file. Once the module is loaded, the promise is resolved and we can use our freshly imported module.

  // src/js/index.js
  import(`./theme-${THEME}`)
    .then((module) => {
      const theme = module.default;
      const title = document.getElementsByTagName('h1')[0];
      
      title.innerText = theme.title;
    });

Alright, so far so good! Now we have two themes and the potential to add even more, by simply creating more SCSS and JavaScript files. There is only one last issue that is troubling: a flash of unstyled content (FOUC) when we first load the page.

FOUC

The css is included in the JavaScript bundle and we see the contents of the page unstyled for a brief moment while the browser fetches and parses the JavaScript bundle.

In order to get rid of it we will use the Extract Text Plugin. What this plugin does is extracting text, css in our case, from our bundle into a separate file.

Let's install the plugin:

  yarn add --dev extract-text-webpack-plugin

Next, let's import it at the top of our webpack.config.js:

  // webpack.config.js
  const ExtractTextPlugin = require("extract-text-webpack-plugin");

Let's add it to our plugins list:

  // webpack.config.js
  new ExtractTextPlugin('styles.css')

Last, we need to update our rules to use the extract text plugin:

  // webpack.config.js
  rules: [{
    test: /\.scss$/, // process styles
    use: ExtractTextPlugin.extract({
      fallback: 'style-loader',
      use: [
      {
        loader: "css-loader"
      },
      {
        loader: "sass-loader", // process scss files and also append some theme data
        options: {
          data: `@import "${path.resolve(__dirname, `./src/styles/theme-${process.env.THEME}.scss`)}";`
        }
      }
    ]})
  }]

Conclusion

With the help of webpack and a few webpack plugins, now we can easily develop UI themes. It might seem like a lot of work upfront, but it will pay off in the long run. We will not have to do manual work updating files every time we need to set a new theme and we have the infrastructure to add more configuration and more themes.