Don't want to code along? Check out the GitHub respository of this post with a full example!
Ant Design is a great UI library for bootstrapping an application quickly. I love it a lot and have been using it to create my student dashboard. However, there is a big disadvantage: lack of dynamic theming. When I faced the need to implement a dark mode for my dashboard I noticed AntD didn't have a provider for styles instead it utilizes Less Stylesheets. This means that in order to modify the variables dynamically you would have to recompile the styles and in runtime, this is not feasible. In this post, I'll explore how I solved this conundrum.
Note: This tutorial will only work with static themes: themes that are just meant to compiled once on the build. If you want to change, for example, the primary color to any color on runtime then you will have to use a library like react-app-rewire-antd-theme.
The Problem
Less stylesheets are really good for supercharging CSS and handle ever-increasing styles. However, the way a React handles this is by using Webpack to compile the styles to CSS and then add them to the build. This means you can't modify its variables or properties as you would do through Less.js.
To fix the lack of modification there exists libraries like react-app-rewire-antd-theme. This injects the less file with the colors to modify directly in HTML and using window.modifyVars
to recompile the styles with the new variables on runtime. This may work with a small number of variables, however, for hundreds of variables for a dark theme, it is just not feasible. It kills the performance of the client.
The Solution
I tried many approaches to solving this issue, from modifying CSS directly to react-app-rewire-antd-theme, and arrived at a solution that is production-ready: swapping between pre-fetched less-compiled CSS stylesheets on runtime using react-css-theme-switch.
Getting AntD Stylesheets
The first thing we have to do is creating the Less stylesheets that will contain AntD styles. To achieve this, we just have to import the variables and styles that we need for each theme. For example, for the light theme:
// light-theme.less
@import '~antd/lib/style/color/colorPalette.less';
@import '~antd/dist/antd.less';
@import '~antd/lib/style/themes/default.less';
// These are shared variables that can be extracted to their own file
@primary-color: #00adb5;
@border-radius-base: 4px;
For the dark theme:
// dark-theme.less
@import '~antd/lib/style/color/colorPalette.less';
@import '~antd/dist/antd.less';
@import '~antd/lib/style/themes/dark.less';
@primary-color: #00adb5;
@border-radius-base: 4px;
@component-background: #303030;
@body-background: #303030;
@popover-background: #303030;
@border-color-base: #6f6c6c;
@border-color-split: #424242;
@table-header-sort-active-bg: #424242;
@card-skeleton-bg: #424242;
@skeleton-color: #424242;
@table-header-sort-active-bg: #424242;
Compiling Less Files With Gulp
On this step, we have to achieve two main objectives: compile Less files to CSS and then append these to the public or static folder so they can be available for the client to be pre-fetched and injected.
There are several ways to compile Less stylesheets: using a task runner, a module bundler, or scripts. Using Webpack would mean creating a plugin and with every change, the styles would have to compile which will slow down development speed for styles that will remain relatively static. Using scripts can result in extensive and unreadable code. Therefore, I decided to go with the Gulp task runner since it applies a pipeline paradigm with files that maintain code simple and maintainable. Also, it has great support for minifying and compiling Less.
Install Gulp and necessary dependencies to minify, use postcss and resolve imports:
yarn add -D gulp gulp-less gulp-postcss gulp-debug gulp-csso autoprefixer less-plugin-npm-import
And create a gulpfile.js
:
const gulp = require('gulp')
const gulpless = require('gulp-less')
const postcss = require('gulp-postcss')
const debug = require('gulp-debug')
var csso = require('gulp-csso')
const autoprefixer = require('autoprefixer')
const NpmImportPlugin = require('less-plugin-npm-import')
gulp.task('less', function () {
const plugins = [autoprefixer()]
return gulp
.src('src/themes/*-theme.less')
.pipe(debug({title: 'Less files:'}))
.pipe(
gulpless({
javascriptEnabled: true,
plugins: [new NpmImportPlugin({prefix: '~'})],
}),
)
.pipe(postcss(plugins))
.pipe(
csso({
debug: true,
}),
)
.pipe(gulp.dest('./public'))
})
Finally, run npx gulp less
, and automatically all styles will be compiled and appended to the public folder.
Changing Between Themes
Now that we have our themes inside the public folder, we can proceed to use them in our app.
First, let's add react-css-theme-switch:
yarn add react-css-theme-switcher
Then, in our index.js
or where we are keeping our providers, we wrap the element with the ThemeSwitcherProvider
. This will store our themes and current theme. Also, allow the use of useThemeSwitcher
which change themes and fetch other metadata:
// index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { ThemeSwitcherProvider } from "react-css-theme-switcher";
const themes = {
dark: `${process.env.PUBLIC_URL}/dark-theme.css`,
light: `${process.env.PUBLIC_URL}/light-theme.css`,
};
ReactDOM.render(
<React.StrictMode>
<ThemeSwitcherProvider themeMap={themes} defaultTheme="light">
<App />
</ThemeSwitcherProvider>
</React.StrictMode>,
document.getElementById("root")
);
Now, we can use it like this:
import React from "react";
import "./App.css";
import { useThemeSwitcher } from "react-css-theme-switcher";
import { Switch, Input } from "antd";
export default function App() {
const [isDarkMode, setIsDarkMode] = React.useState();
const { switcher, currentTheme, status, themes } = useThemeSwitcher();
const toggleTheme = (isChecked) => {
setIsDarkMode(isChecked);
switcher({ theme: isChecked ? themes.dark : themes.light });
};
// Avoid theme change flicker
if (status === "loading") {
return null;
}
return (
<div className="main fade-in">
<h1>The current theme is: {currentTheme}</h1>
<Switch checked={isDarkMode} onChange={toggleTheme} />
<Input
style={{ width: 300, marginTop: 30 }}
placeholder="I will change with the theme!"
/>
</div>
);
}
Inside our playground, we will be able to change smoothly between themes!
CSS Injection Order
You may notice our styles are being appended to the head on runtime which means that those that were added on build time will be possibly overridden.
On this screenshot, you may observe that the style tags which are build-time CSS declarations are located before those pre-fetched. To avoid overriding our styles, we can mark an insertion point in our HTML which will allow it to be injected at that location, avoiding collisions. Thankfully, we don't have to implement it ourselves since react-css-theme-switcher comes with a built-in solution.
To begin, add in your HTML a comment where you want the styles to be injected. Also, the text content of this will be the identifier we will pass to the ThemeSwitcherProvider
.
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
...
<title>React App</title>
<!--styles-insertion-point-->
</head>
<body>
...
</body>
</html>
Finally, on the provider:
// index.js
ReactDOM.render(
<React.StrictMode>
<ThemeSwitcherProvider
themeMap={themes}
defaultTheme="light"
insertionPoint="styles-insertion-point"
>
<App />
</ThemeSwitcherProvider>
</React.StrictMode>,
document.getElementById("root")
);
With this, we are done! If you check your developer tools:
Our styles will be injected where we want them to. Now, it won't conflict with our styles!
Note: Watch out for HTML comment trim in production. Make sure your project works well in production and in development.
Conclusion
AntD is an amazing UI library which can help to create projects quickly and with a beautiful user interface. However, it has a major weakness: it's styles are not easily configurable since they use Less stylesheets. Using Gulp to compile and extract AntD CSS and react-css-theme-switcher to inject and change between them, this issue could be solved in a performant way. In the future, it would be good to explore how to optimize these generated stylesheets by extracting duplicate styles and reducing fetched CSS size.
I hope you found this article useful. For more up-to-date web development content, follow me on Twitter, and Dev.to! Thanks for reading! 😎
Did you know I have a newsletter? 📬
If you want to get notified when I publish new blog posts and receive an awesome weekly resource to stay ahead in web development, head over to https://jfelix.info/newsletter.
Top comments (7)
Hi Jose, thanks for your post.
Do you have any idea how to implement it with Next.js?
I believe Next.js would work just fine with it. Give it a try and, please, let me know if you have any problems. Cheers 😎
I was looking for an approach without using Gulp. Do you think this can be done just by Webpack inside
next.config.js
?Yes, you can even do it with vanilla Javascript!
How? (:
It depends on how you decide to do it. If it is with vanilla Javascript (node), check out FS operations. On the other hand, for Webpack check out their documentation on writing a plugin.
Hi Jose, I found this super helpful!
Do you any ideas or suggestions on how to keep a user's theme choice persistent through page refreshes and different visits? I'm assuming localstorage, but not quite sure how to implement, any help would be greatly appreciated! :)