Recently we've implemented a feature for a customer who wanted to switch between a light and a dark theme on his website. The only preconditions/requirements were:
The app is written in react.
The Ant Design component library is used (v4.9.1).
A customer identity guideline demands custom styles for both themes (eg. colors, font-size, ...)
My proposed solution
Switching between themes should be as easy as possible. So what I was aiming at here was setting a css class at the top level (eg. light or dark), which tells all its children how they need to be styled.
To accomplish that, we'll have to prepend a class selector to every css rule provided by Ant Design. This way of scoping can be done by simply nesting the css selectors inside the according prefix selector.
In a previous version of this post I used PostCSS PrefixWrap. But thankfully @nring reminded me of the nesting capabilities of less selectors. This way we don't need yet another library.
So given the following css rule
.antd-btn{color:'blue'}
nesting it inside a .light-class will turn it into:
.light.antd-btn{color:'blue'}
This form of scoping is exactly what we want. The following tutorial shows you how we can use this for supporting theme switching in a react application.
First we'll have a look at how we can customize the default Ant Design theme. Luckily they provide a tutorial for that. Ant Design is using less for defining style variables that are then used by the theme. Overwriting those variables will change the appearance of all the Ant Design components.
The tutorial gave us the following options for customization:
Modifying the theme with webpack by using modifyVars
The modifyVar option lets you modify the less variables by overwriting them in a method parameter. This method must be used in the webpack loader for less files to modify the theme during build time.
Building the project with webpack will then load the Ant Design less file, modify the variables and subsequently generate a css stylesheet for the app. The result is a single css file containing the customized theme.
For this to work, we need to eject the webpack configuration from the create-react-app generated project. In case you don't want to eject it, you can use a plugin like craco which lets you hook into the webpack configuration (with limited options).
Nevertheless both options generate a single static file. Since we want to switch between two themes with different customizations, this isn't a suitable option.
umi
If you're using umi, you have the possibility to provide modifications in a config file. umi is a enterprise-class front-end application framework and provides many features out of the box. But since the customer wanted his project to be plain and simple, we refrained from adding this dependency as we wont use many of those features. Therefore this wasn't an option either.
less files
This leaves us with the last option of creating separate less files. Each less file basically imports the default configuration and design rules. By overriding the variables, each theme can be styled accordingly. This is the approach I'll describe in the next sections.
So, let's hit it off!
Creating the theme files
Creating a customized theme file only requires two steps:
Importing the Ant Design less file
Overwriting the variables
In my case the theme file for the light mode looks like this:
As you can see the file consists of a theme-selector which encompasses all the Ant Design styles and styling overrides.
The first import gives us all the default definitions for the styling variables. You can also follow this reference to have a look at all the styling possibilities.
The second one imports all the Ant Design styles that make use of the variable definitions.
In the following lines you can overwrite the default variables by simply assigning a new value to them. In this case we've overwritten the body background to a light grey color.
The same can be done for the dark theme. If you want to see the content of that, just follow the link to my github repo at the bottom of the page.
Compiling the theme files
The next step is to generate css files from the less files by compiling them. Per default react only supports styling with css and sass. Supporting less will require a modification to the webpack configuration. If you built your app using create-react-app you need to eject it first to get access to the webpack config file. You might be able to modify the webpack pipeline using configuration tools (like rewired, craco), but those tools are mostly community driven (so stuff can break). But the decision of ejecting create-react-apps is mostly a matter of taste. I wouldn't recommend ejecting apps to junior developers with little webpack experience.
For this post I decided to eject the application. To add the support for less files, you'll have to do the following steps:
Eject the app (if you're using create-react-app)
Install the required packages
Modify the webpack configuration
Eject the app
By ejecting the app you get access to all the configuration and build stuff that create-react-app has hidden from you. Please mind that this step is permanent, because your configuration changes can't be converted back to the create-react-app structure.
Just run the following command:
npm run eject
Install the required packages
Run the following command to install all the required packages:
npm i -S less less-loader@7
⚠️ Please mind that I specified the version 7 for the less-loader. This is currently required for apps created via create-react-app, because they still use webpack 4 which isn't compatible with the latest less-loader.
Those packages include:
less: The compiler that will turn your less files into css.
less-loader: The webpack loader that tells webpack how to process the less files.
Modify the webpack configuration
Next you'll need to tell webpack how to process those less files.
Stylesheets are compiled and loaded into the dom with webpack loaders. Most of the time you'll need multiple loaders that are chained together. Each loader is responsible for a specific task (eg. Compiling SASS -> CSS, Injecting CSS into the DOM, Provinding CSS in separate files, ...). Thankfully create-react-app is already setting up all those loaders with a helper function. In order to be able reuse this helper with our less stylesheets we just have to extend it a little bit.
Since Ant Design requires javascript to be enabled to properly compile the less styles, we need to be able to configure the less loader. This is usually done by passing options to the less loader. In order to do this with our existing helper function, we just have to add an optional parameter for those options and expand it inside the pre-processor options. It must be optional because the other registered loaders don't use additional options (eg. sass).
Next we can use this helper function to create the loaders for the less files. Just append the following rule beneath the other styling rules (css, sass) of your webpack configuration:
This rule consists of:
a regex to match a specific file,
the loaders which we gather using our helper function,
a flag that marks our rule as having side effects.
The first parameter we pass to the loader helper is the object containing options for the css-loader. Those options configure the use of source-map files as well as the number of processors that are run before the css-loader (in this case its the less-loader and the postcss-loader).
The second parameter is the less-loader that will convert less files to css and load them into the DOM.
The last parameter contains the additional options that are passed to the less-loader.
Marking them as having side effects, will prevent the tree shaking process from pruning them.
Implementing the theme switcher
The last step is pretty simple. All we need is to conditionally set a class to any top level DOM element.
For this example we'll add the theme switcher to the App component in the App.js file.
First of all we'll have to reference the two less files, so the webpack bundler can get a hold of them.
As a next step we'll make use of a react hook to set the theme state to either light or dark. An effect that listens on changes to this theme state will then update the class list of the body to eigher light or dark. Now you're able to switch themes!
Conclusion
Setting up a theme switcher can be pretty simple. If you take a look at my git commit you can see that it mainly affected two files (webpack.config.js and App.js). I have seen solutions on the web that were using gulp/grunt for building the css files. But since the react app is already based on webpack, adding another build tool seemed like an overkill. Especially since webpack already provides things like hashing/injecting which might be more complex with other build runners.
Some areas of improvement are:
Dynamically load light/dark theme: At the moment webpack will create a single css file containing all the styles (light AND dark theme). This means that the user will always fetch both themes, even if he never changes them. This can be changed by dynamically importing the according css file though.
Storing the selected theme: Once the user selected a theme it could be persisted, so we can use it on his next visit. In this linked commit you can see how I used the localStorage for persisting the selection.
See the code
In case you want to see how all those pieces fit together, you can have a look at the github repo I published. In commit #f9edd75 you can see all the changes that are relevant for this tutorial. The project was already ejected, so I could keep this commit small and clean.
Scoping via less works by nesting all the ant design style imports into a .light/.dark-class. At the moment I didn't implement lazy loading or storing the current theme. But it's basically them same as with the PostCSS solution.