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