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.
To see my approach in action, just have a look at the following page: chrsi/antd-theme-switcher-example
Customizing an Ant Design theme
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
- Using a config file in umi
- Creating separate less files
Let's analyze those options:
modifyVar
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.
chrsi / antd-theme-switcher-example
An example project that implements theme switching with the ant design ui library
Theme Switcher with Ant Design
This example should demonstrate to you how a theme switcher can be implemented for the Ant Design component library.
You can see it in action on here.
Please also take a look at my dev.to Post. It describes the approach in more detail.
Implementation Variants
I implemented/extended theme switchin in various ways. Scoping via Less is the preferred way because it doesn't use another library for scoping.
Scoping via Less
Branch: less (default)
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.
Scoping via PostCSS PrefixCSS
Branch: postcss
This is an outdated solution that used the PrefixCSS functionality of PostCSS to prefix every ant design styling rule with either .light
or…
This post came out longer than expected. I hope you enjoyed it nevertheless. Thanks for bearing with me and have fun using your new Theme Switcher.
Top comments (7)
Thanks for the detailed background info on this! I'm currently in the process of adapting this approach for a Next.js app with varying levels of success. I was wondering if you found that you needed to use PostCSS to namespace the stylesheets rather than just wrap the individual theme files like so:
I haven't had 100% success with the latter approach, but jerry-rigging PostCSS into an already jerry-rigged Less config for Next.js hasn't been the most straightforward thing to do.
Hey, thanks for the heads-up! Somehow I haven't thought about the nesting capabilities of less... But yes that should work and would be way better than using another webpack plugin 😅
I'll try it on the weekend and might update the post accordingly
I just updated the post. Thanks again :)
Hey @chrsi ,
Thank you for this awesome post.
after implementing everything i realized that some of ant design components breaks and not acting as they should,
i've noticed "notification" and "tooltip" losses some styling behaviors.
do you have an idea how this can be solved ?
Thanks :)
Hey @christian , thanks for the great post.
I tired following your post, but I am facing some issue while starting the application, could you please help me with that.
./src/themes/theme.light.less (./node_modules/css-loader/dist/cjs.js??ref--5-oneOf-9-1!./node_modules/postcss-loader/src??ref--5-oneOf-9-2!./node_modules/less-loader/dist/cjs.js??ref--5-oneOf-9-3!./src/themes/theme.light.less)
TypeError: this.getOptions is not a function
Hello @jjaijg ,
sorry i couldn't get to you any earlier. What version of webpack, css-loader, less-loader and postcss-loader are you using?
According to github.com/webpack-contrib/less-lo... it seems like less-loader v8 requires at least webpack v5. It was released Feb 1, which might have caused your issue. So maybe upgrading webpack oder downgrading less-loader to v7.x.x might solve the issue.
Found some issue with layout component. i think you must import @import '~antd/dist/antd.less' or something else. i didn't fidured out. Also how to handle icon colors