Webpack 2 & Semantic UI Theming

TL;DR: This guide is tested on Webpack 3 as well. Working example here


One of the most frustrating things i recently faced was trying to setup a custom semantic UI theme with Webpack. The end goal was to be able to create a theme configuration that would extend Semantic’s defaults, while changing some variables that the outputted semantic .css files depend upon. The alternative would be of course to create .css files that would override the default CSS declarations of Semantic, but that seemed too messy for me.

Turns out this thing is way more complicated that it should be and on top of that there is no article out there focusing on Webpack 2. Thus, I decided to create a simple tutorial on how to make it work. Before we begin i’d like to mention that some of the steps here are taken from neekey’s guide that you can find here:

Installation

The first step would be to install the LESS version of Semantic UI via npm:

npm install --save semantic-ui-less

Because we will need to compile .less files we need an appropriate loader to handle those files. If you have one already configured then that’s perfect. If you don’t, simply add a rule within the module entry of your webpack configuration along with the corresponding plugin for handling the .css output.

// at the top of your webpack configuration file
const
ExtractTextPlugin = require('extract-text-webpack-plugin');
...
module: {
rules: [
...
// this handles .less translation
{
use: ExtractTextPlugin.extract({
use: ['css-loader', 'less-loader']
}),
test: /\.less$/
},
...
]
},
plugins: [
...
// this handles the bundled .css output file
new ExtractTextPlugin({
filename: '[name].[contenthash].css',
}),
...
]

This would of course require some additional plugins & loaders. If you don’t already have them installed you can easily add them with the following command:

npm install --save-dev less css-loader less-loader extract-text-webpack-plugin
/*
09/04/2018: Apparently some people mentioned that less-loader v4.0.5 or greater introduces a bug. If this happens to you as well, just make sure you install v4.0.4 or older.
*/

Because there are a lot of icons & images, you must make sure that your webpack configuration know’s how to bundle them. An example configuration that you should follow if you don’t have anything set up, would be the following:

...
module: {
rules: [
...

// this rule handles images
{
test: /\.jpe?g$|\.gif$|\.ico$|\.png$|\.svg$/,
use: 'file-loader?name=[name].[ext]?[hash]'
},

// the following 3 rules handle font extraction
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: 'url-loader?limit=10000&mimetype=application/font-woff'
},

{
test: /\.(ttf|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: 'file-loader'
},
    {
test: /\.otf(\?.*)?$/,
use: 'file-loader?name=/fonts/[name]. [ext]&mimetype=application/font-otf'
}
]
}

Which would of course depend on the url-loader and file-loader loaders of Webpack. If you don’t have them listed on your dependencies you can easily add them by entering the following:

npm install --save-dev file-loader url-loader

Creating a theme folder

The next step would be to create a folder for our theme. Within your project root, in the same level where the node_modules directory is located, create a folder and name it my-semantic-theme or whatever name you want. Inside it create a file and specifically name it theme.config ( this is NOT a .js file!). In addition, create a folder (direcly inside my-semantic-theme) and name it site. Your project will look something like that

...
node_modules/
my-semantic-theme/
site/
theme.config

In order for our theme to extend the default theme of Semantic UI we need to (unfortunately) copy and paste the contents of the default theme configuration located in node_modules/semantic-ui-less/theme.config.example. Simply open it, copy everything and paste it into our newly created theme.config file. After that we have to make 3 big changes at the very bottom of the file.

  1. Change @import "theme.less"; to @import "~semantic-ui-less/theme.less";
  2. Change @siteFolder : "site"; to @siteFolder : "../../my-semantic-theme/site";replacing “my-semantic-theme” with the name of your folder.
  3. Add @fontPath : "../../../themes/@{theme}/assets/fonts"; at the very end of theme.config (as the last line)

What we accomplished here, is to map the Semantic-UI assets to the ones we have created. The first change makes sure that we import the correct theme.less file from the node_modules. The second change makes sure that the siteFolder variable points to our site folder, with the starting directory being /node_modules/semantic-ui-less/ (hence the relative path). The third change makes sure that Semantic-UI can find the fonts that already exist in the themes.

The final step that we have to solve ( and that was actually the hardest to locate) is telling Semantic UI to use our theme.config file when building the .css files. Because semantic-ui-less was not built to be easily extendable it always searches for the theme.config file within its root package directory using a relative path. What we must do it tell webpack “Hey, when someone requests a relative path to theme.config, map it directly to the theme.config that we just created”. To do that we must add an alias. In a Webpack 2 configuration , supposing that your webpack configuration file is in the same level as the my-semantic-theme directory, that would look like that:

// at the top of the webpack config file
const path = require('path');
module.exports = {
entry: ...,
output: {
...
},
resolve: {
alias: {
'../../theme.config$': path.join(__dirname, 'my-semantic-theme/theme.config')
}
},
module: {
...
}
}

That tells webpack to resolve all the “../../theme.config” import calls to our specific theme.config file.

That’s it. Now within the site folder that we created, you can create all the folders that a normal theme has. That means your site folder will most likely contain the following folders.

site/
assets/
collections/
elements/
globals/
modules/
views/

Just visit the official repo to see how you can change all the variables you might want. Since we are extending the default theme, you only need to add the variables you want to change. All the rest will keep their default values.

In order for the changes to be bundled, you have to — of course — import the semantic UI .less library somewhere within your webpack entry point. So if your entry point is app.js, then simply add the following line at the top of your file:

import "semantic-ui-less/semantic.less"; // ES6
or
require("semantic-ui-less/semantic.less") // CommonJS

Running webpack will result into the proper CSS files that you wanted based on your theme. You can test that by changing a variable value like the default background color of the buttons .


Bundling Individual Components

By default, the CSS of every Semantic UI component will be produced and bundled from the corresponding LESS files, but there are times when you’ ll only be using a handful of Semantic components within your project. In order to reduce the overall size of your bundles you should only bundle the CSS of the components that you’ ll be using. There are 5 simple steps to achieve that.

  1. First create a file named semantic.less within the my-semantic-theme folder. This folder should now look something like that:
...
node_modules/
my-semantic-theme/
site/
theme.config
semantic.less

2. Navigate to the library’s corresponding file located under node_modules/semantic-ui-less/semantic.less, copy all contents of this file and paste them into our newly created semantic.less file. Our file should look something like that:

...
...
...
/* Global */
& { @import "definitions/globals/reset"; }
& { @import "definitions/globals/site"; }
/* Elements */
& { @import "definitions/elements/button"; }
& { @import "definitions/elements/container"; }
& { @import "definitions/elements/divider"; }
& { @import "definitions/elements/flag"; }
& { @import "definitions/elements/header"; }
& { @import "definitions/elements/icon"; }
...
...
...

3. Keep all the lines that correspond to components that you will be using in your project and delete all the rest (you should keep the globals ).

4. Replace all & { @import "definitions/..." with & { @import "~semantic-ui-less/definitions/..."

5. At your webpack entry point, you must now include the newly created semantic.less instead of the library’s default one. Following the same project structure, this would be achieved through the following command:

import "../my-semantic-theme/semantic.less"; // ES6
or
require("../my-semantic-theme/semantic.less") // CommonJS

You’ re all set! Now only the CSS of your chosen components will be produced and bundled!

You can find a full example of all of the above in this repo

Cheers :)