(or: how to use Less CSS with create-react-app (but really, stick to cra defaults))
Note: Using create-react-app@4? While this guide works, there's a slightly simpler one available here.
There are many guides showing how to customize antd in a create-react-app out there, but so far I have not found any that go without using craco, react-app-rewired or similar solutions to mess with cra's default config.
This guide will show you how to customize antd without adding any dependencies besides less-loader while completely staying in the bounds that create-react-app provides.
- Install
less-loader
npm install --save less-loader less
- Replace your
antd(.min).css
import a nwith the following line:import "./customized-antd.css";
- Create a file called
customized-antd.css
with the following content:@import "./antd-overrides.css!=!less-loader?{\"lessOptions\":{\"javascriptEnabled\":true}}!antd-overrides.less";
- Create a file called
antd-overrides.less
with the following content:@import "~antd/lib/style/themes/default.less"; @import "~antd/dist/antd.less"; // set Less variables here like this: @primary-color: #61a70e; @border-radius-base: 10px;
While antd provides a sound design system to start off, there comes a time when you may want to change the visual design, maybe just to switch colors.
If you use create-react-app (cra for short) to build your React app, you are faced with the question whether to use one of the many guides and solutions to meddle with cra intenrals, or completely eject from cra just to change antd's color scheme.
There is a third option and this guide will show how to customize antd when using cra
- without flaky packages that break on every other major version of cra
- no bogus post-processing that breaks my hot-reloading
- full compatibility with the rest of cra
The "convention over configuration" aproach of create-react-app saves a lot of time. It helps developers to get started very fast without spending much time on their toolchain and for most use-cases, batteries are included. But using third party libraries that do not follow the convention may become quite frustrating.
A popular and very comprehensive UI component framework is antd. It consists of many well-designed components to use in more complex React-based software and also allows for quite some customizations to its visual design.
However, it is using Less CSS for styling and customization is done by
modifying Less variables, either in an imported .less
file or by setting
them in the Less pre-processor.
Either way, we have to interpret Less in order to customize antd.
While cra readily supports Sass/SCSS right out of the box, there is no way to use Less in cra. While there is an unmerged pull request, it may well be several months before we see Less support in cra.
Because of this, many different ways have evolved to use Less in cra, each with their own drawbacks.
All those solutions have one thing in common:
Stuff can break
In cra, webpack is used to convert all your source files to a working
application. Besides bundling together all the JavaScript files it also
processes them for minification, browser-compatibility and other stuff.
Those transformations are done by webpack using loaders which are hooked
into import
statements. They are the reason why you can do the following
and get your CSS classnames as globally unique identifiers:
import styles from "./somefile.css";
As all imports go through a chain of loaders, many approaches
get Less working with cra by modifying the internal webpack configuration
of cra to process .less
files with the less-loader
.
As this configuration is not intended by cra to be changed, all
existing solutions based on craco or similar tools use some sort of
configuration merging by hooking into cra internals. They bring their
own plugin ecosystem that may interfere with normal cra operations
and may even break with a minor cra update.
In webpack, we can get around using a custom configuration file by using a special inline syntax to configure loaders. This syntax is not very useful as a replacement for the configuration file in the general case, hence it is not recommended, but it is still officially supported and very useful now that we want to use a loader without touching the configuration.
As of version 5 of webpack, there is another mechanism,
Asset Modules which partially
replaces but also partially interferes with the regular loader mechanism.
As described in the configuration, Asset Modules allow for including auxiliary
files without a specific loader, either by referencing a filename or by directly
inlining the file. In cra@5, all resources that are not directly supported by cra,
i.e. a file with the extension .less
is forced to be a asset/resource
module,
which causes only the filename/url to be imported instead of the while file. While
this is useful when embedding images or audio files in <img>
or <audio>
elements,
this is not what we want with stylesheets.
Let's start with a plain cra app and add some antd:
$ npm init react-app@4 cra-antd-customization
$ npm install --save antd
The straightforward way to include antd's styles into your app is to use the already precompiled stylesheet that antd distributes:
import "antd/dist/antd.min.css";
With this line at the top of your App.js
or index.js
you can use any antd
component without further ado and it will look like this:
But we are importing a plain .css
file. No processing necessary, but no design
customization possible. We first need something that can understand
Less, and since we want to make use of all the webpack glory, we will
use less-loader
:
$ npm install --save less-loader less
The antd less configuration site tells us how to use this loader:
- we need to chain it through
style-loader
,css-loader
and finallyless-loader
- and we have to activate the
javascriptEnabled
option
Normally, we could just use webpack's inline syntax to configure this loader chain
and use ?
to pass options to the less-loader:
// this won't work with cra@5
import "!style-loader!css-loader!less-loader?{\"lessOptions\":{\"javascriptEnabled\":true}}!antd/dist/antd.less";
In this chain it would be style-loader's responsibility to take a stylesheet and
apply it to the DOM's <head>
element accordingly.
However, as cra@5's config only yields filenames for .less
files, style-loader
won't get a complete stylesheet that it can apply to the DOM but only a filename,
it doesn't really know what to do.
Where can we put a filename to a stylesheet then?
We can put it in an @import
clause in another stylesheet and import this one
from our App.js
So let's create a new file, called customized-antd.css
and add the following line:
/* still not really there, yet */
@import "!css-loader!less-loader?{\"lessOptions\":{\"javascriptEnabled\":true}}!antd/dist/antd.less";
What happened to style-loader
? Well, it imports styles as JavaScript.
Since we are not in a JavaScript file, we don't need it, we only want to import
CSS here.
But this does not work either, because webpack processes all imports, even
those in CSS files, and instead importing the stylesheet that is generated by
less-loader, the import is literally replaced by the filename (without @import
in front of it). A file containing just a filename is not a valid CSS stylesheet
and importing it won't do anything useful.
Luckily, there is another handy webpack import syntax, the
inline matchResource
syntax. It tells webpack to import something as if it had a different
name. And we can use this syntax to import the .less
file as if it was
a CSS file. And webpack/cra@5's config knows how to handle CSS files!
Combining this syntax with our inline configuration for less-loader, we can write:
@import "antd.less.css!=!less-loader?{\"lessOptions\":{\"javascriptEnabled\":true}}!antd/dist/antd.less";
What happened to all the other loaders? Well, for one, style-loader
is
really not useful in a CSS file, because it generates JavaScript to import a
stylesheet into the DOM and we can't process that JavaScript inside a CSS file.
Then there's the "as if" syntax which tells webpack to use all the standard
loaders associated with a CSS file which, unsurprisingly, contains css-loader.
We can then just import "customized-antd.css";
in our App.js
and finally
we can, with a little indirection, import .less
files into a cra App!
The inline loader syntax, while being non-obvious, is quite easily explained
and you only have to copy it into your code once.
The syntax for loader parameters, however, is quite cumbersome with all the
\"
s and the object notation for each and every variable.
If you want to do some heavy modification, this quickly becomes
unmaintainable in a single line if you use the modifyVars
parameter
as suggested by antd's documentation.
Instead of configuring all overrides in the loader configuration, we can
also just write our own .less
file and set the variables there.
So let's create a override-antd.less
file like this:
@import "~antd/lib/style/themes/default.less";
@import "~antd/dist/antd.less";
@primary-color: #61a70e;
@border-radius-base: 10px;
and then back in our customized-antd.css
file, change the filename
for the import:
@import "override-antd.less.css!=!less-loader?{\"lessOptions\":{\"javascriptEnabled\":true}}!./override-antd.less";
This way we have a single file where we can override all the Less variables we want and can make antd look like this:
I'd still recommend using SCSS if you want to introduce your own styles because this is the way cra promotes.