Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save Kruemelkatze/057f01b8e15216ae707dc7e6c9061ef7 to your computer and use it in GitHub Desktop.
Save Kruemelkatze/057f01b8e15216ae707dc7e6c9061ef7 to your computer and use it in GitHub Desktop.
Theming Ant Design with Sass and Webpack

Theming Ant Design with Sass and Webpack

This is a solution on how to theme/customize Ant Design (which is written in Less) with Sass and webpack. Ant itself offers two solutions and a related article on theming, but these are only applicable if you use Less, the antd-init boilerplate or dva-cli.

What this solution offers:

  • use a single sass-file to customize (no duplicate variables for your project and Ant)
  • hot reload compatibility
  • no dependencies on outdated npm modules
  • easy integration with your existing webpack setup (webpack 3+ tested)

Any downsides?

  • all ant-styles are imported

According to our observations this does not have that much impact in practice, as using only a couple of Ant components will already result in most styles being loaded.

Procedure

The main procedure is as follows:

  1. Define your variables in variables.scss
  2. Write a ant.less file which imports the Ant less-styles and your sass-variables (yes)
  3. Tell webpack to load less files via less-loader
  4. Tell webpack to rewrite sass-files which are imported from less-files ($something -> @something)
  5. Import ant.less in your project.

How?

Apply the changes / create the files as shown below. The changes should be self-explanatory. Just keep in mind, that your exact webpack-setup may differ a bit from the one given below.

Authors: Kruemelkatze, mrukas

//Use .babelrc or add the options to the webpack loader
//Remove comments in .babelrc as they most likely will produce build errors
{
"presets": [
//...
],
"plugins": [
//...
["import", {
"libraryName": "antd",
"libraryDirectory": "es" //or "lib" for default
//No "style" setting
}]
]
}
@import "~antd/dist/antd.less";
@import "variables.scss"; // <-- SASS in a LESS file!
// Import the ant.less file in your project
import 'ant.less';
// This loader will simply replace all $something sass-variable with @something less-variables
module.exports = function (source) {
return source.replace(/\$/ig, '@');
};
// THIS FILE SHOULD ONLY CONTAIN VARIABLE DEFINITIONS
// Ant Colors & Styles can be overwritten here. Just use the variable names from
// https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less
// Be sure to use $xyz, not @xyz!
$primary-color: crimson;
module: {
rules: [
{
loader: 'babel-loader',
test: /\.jsx?$/,
// Other settings, like include or exclude
options: {
presets: [
//...
],
plugins: [
// ...
// Importing Ant here is not needed if you are using a .babelrc file
['import', {
"libraryName": "antd",
"libraryDirectory": "es" // or "lib" for default
// No "style" setting
}],
]
},
},
// ...
// Include less-loader (exact settings may deviate depending on your building/bundling procedure)
{
test: /\.less$/,
use: [
{ loader: "style-loader" },
{ loader: "css-loader" },
{
loader: "less-loader",
options: {
javascriptEnabled: true //This is important!
}
}
]
},
// Tell the DEFAULT sass-rule to ignore being used for sass imports in less files (sounds weird)
{
test: /\.scss$/,
issuer: {
exclude: /\.less$/,
},
// ... other settings
},
// Define a second rule for only being used from less files
// This rule will only be used for converting our sass-variables to less-variables
{
test: /\.scss$/,
issuer: /\.less$/,
use: {
loader: './sassVarsToLess.js' // Change path if necessary
}
},
// ...
]
}
@samcyn
Copy link

samcyn commented Apr 3, 2019

Hi I tried to implement this in nuxt js and it didn't work. Is this only working with react and next js?

@Kruemelkatze
Copy link
Author

Hi I'm tried to implement this in nuxt js and it didn't work. Is this only working with react and next js?

Hey, sorry for the late answer. We did not use Next js, Nuxt js or any other frameworks. Only webpack + React + Antd

@vrsotto
Copy link

vrsotto commented Sep 7, 2019

can i Use this with create-react-app?

@mrukas
Copy link

mrukas commented Sep 7, 2019

Yes it is possible to use it with create react app. I'm not sure if there is another way than ejecting (https://create-react-app.dev/docs/alternatives-to-ejecting). Maybe you can customize the webpack.config otherwise. But ejecting should definitely work. Please make sure you know what you do before ejecting.

@antoinm
Copy link

antoinm commented Oct 4, 2019

@vrsotto I just managed to get it working with CRA TS.

Here is what I've done:

  • Install react-app-rewired, customize-cra, less and less-loader
  • Modify the scripts in the package.json to use react-app-rewired
  • Create a config-override.js just like this:
const {
  override,
  fixBabelImports,
  addLessLoader,
  addWebpackModuleRule
} = require("customize-cra");

module.exports = override(
  fixBabelImports("import", {
    libraryName: "antd",
    libraryDirectory: "es",
    style: true
  }),
  addLessLoader({
    javascriptEnabled: true
  }),
  addWebpackModuleRule(
    // This rule will only be used for converting our sass-variables to less-variables
    {
      test: /\.scss$/,
      issuer: /\.less$/,
      use: {
        loader: "./sassVarsToLess.js" // Change path if necessary
      }
    }
  )
);
  • Follow the steps listed in the original post BUT skip the .babelrc and webpack.config.js files.

I hope this will help you.

@vrsotto
Copy link

vrsotto commented Oct 5, 2019

@vrsotto I just managed to get it working with CRA TS.

Here is what I've done:

  • Install react-app-rewired, customize-cra, less and less-loader
  • Modify the scripts in the package.json to use react-app-rewired
  • Create a config-override.js just like this:
const {
  override,
  fixBabelImports,
  addLessLoader,
  addWebpackModuleRule
} = require("customize-cra");

module.exports = override(
  fixBabelImports("import", {
    libraryName: "antd",
    libraryDirectory: "es",
    style: true
  }),
  addLessLoader({
    javascriptEnabled: true
  }),
  addWebpackModuleRule(
    // This rule will only be used for converting our sass-variables to less-variables
    {
      test: /\.scss$/,
      issuer: /\.less$/,
      use: {
        loader: "./sassVarsToLess.js" // Change path if necessary
      }
    }
  )
);
  • Follow the steps listed in the original post BUT skip the .babelrc and webpack.config.js files.

I hope this will help you.

Thanks @antoinm but i got it working through this blog https://dailyjsx.com/antd-rescript-sass-boilerplate/

@AlexeyDrobot
Copy link

Thanks for this way. Is there any way to avoid duplication of styles? https://take.ms/8vOAP

@Kruemelkatze
Copy link
Author

Thanks for this way. Is there any way to avoid duplication of styles? https://take.ms/8vOAP

Hmm, we did not encounter this in our apps. Do you maybe still have the antd styles imported a second time somewhere in your .js or .jsx?

@AlexeyDrobot
Copy link

I did it on a clean project. create-react-app
And I was convinced that I connect ant.less once. No more. Any other ideas?

@adomrockie
Copy link

I did it on a clean project. create-react-app
And I was convinced that I connect ant.less once. No more. Any other ideas?

Did you get this to work? Having the same issue.

@Kruemelkatze
Copy link
Author

We will have a look on this and then report here! :)

@adomrockie
Copy link

We will have a look on this and then report here! :)

Thanks, must be something we over looking but I have checked my code base and I only use ant.less once.

@xaxist
Copy link

xaxist commented Apr 8, 2020

Do we have any working example using NextJs framework? I am trying to use the in-built Sass feature available in NextJs 9.3 and need to convert Ant's less variables to Sass variables.

@Kruemelkatze
Copy link
Author

We will have a look on this and then report here! :)

Thanks, must be something we over looking but I have checked my code base and I only use ant.less once.

Sorry for not getting back to you! Was quite some troubling time, but we will check this. Unfortunately, we stopped using this approach in our company, as we migrated to an unejected CRA boilerplate and didn't use theming that much anyway. So I'm not totally up to date on this. ;)

Do we have any working example using NextJs framework? I am trying to use the in-built Sass feature available in NextJs 9.3 and need to convert Ant's less variables to Sass variables.

I haven't used NextJs before, sorry. But it looks like they allow for a custom webpack config and babel config Maybe you can add the rules and loaders there?

@xaxist
Copy link

xaxist commented Apr 8, 2020

Do we have any working example using NextJs framework? I am trying to use the in-built Sass feature available in NextJs 9.3 and need to convert Ant's less variables to Sass variables.

I haven't used NextJs before, sorry. But it looks like they allow for a custom webpack config and babel config Maybe you can add the rules and loaders there?

Thanks for the quick revert. I tried configuring the webpack with NextJS, but the moment we less-loader in the config, NextJs disables the inbuilt sass support. I guess I should ask the NextJs team if they can come up with a workaround for this one. Thanks for your time :)

@pabloimrik17
Copy link

My build is failing since i updated to less-loader@6.1, in the older version (5.0) was working fine.

image

Any idea?

@Kruemelkatze
Copy link
Author

It looks like the sass-loader is not correctly called by the less-loader when processing the sass file. I had a look at the less-loade changelog for any obvious breaking changes but didn't see anything.

Do you still have this rule in your webpack config? Asking just in case if it got removed unintentionally when upgrading your project. :-)

    // Define a second rule for only being used from less files
    // This rule will only be used for converting our sass-variables to less-variables
    {
      test: /\.scss$/,
      issuer: /\.less$/,
      use: {
        loader: './sassVarsToLess.js' // Change path if necessary
      }
    },

Any idea on this @mrukas?

@pabloimrik17
Copy link

It looks like the sass-loader is not correctly called by the less-loader when processing the sass file. I had a look at the less-loade changelog for any obvious breaking changes but didn't see anything.

Do you still have this rule in your webpack config? Asking just in case if it got removed unintentionally when upgrading your project. :-)

    // Define a second rule for only being used from less files
    // This rule will only be used for converting our sass-variables to less-variables
    {
      test: /\.scss$/,
      issuer: /\.less$/,
      use: {
        loader: './sassVarsToLess.js' // Change path if necessary
      }
    },

Any idea on this @mrukas?

Hi, everything in my project remains the same, i only updated from antd 4.0.2 to 4.3.4 and less-loader from 5.0.0 to 6.1. And related to less-loader changed the options config to lessOptions in the webpack file.

image

@jgroh9
Copy link

jgroh9 commented Aug 25, 2020

@pabloimrik17 I'm having the same issue. Were you able to solve the problem? If so, would you be able to post the solution?

@sametsafak
Copy link

sametsafak commented Dec 13, 2020

How can I make this work in vue.config.js file? (it's using webpack-chain)
I tried config below but can't understand why it's not working.

// vue.config.js

module.exports = {
  // this one not working with ant.less file. If I removed issuer, include and end lines,
  // It's working for every scss related sections even in .vue files
  chainWebpack: config => {
    config.module
      .rule("less2")
      .issuer()
      .include.add([require.resolve("./src/styles/ant.less")])
      .end()
      .test(/\.scss$/)
      .use("./sassVarsToLess.js")
      .loader("./sassVarsToLess.js")
      .end();

    // this is working I guess, because when I use @primary-color: red; in variables.scss file, 
    // it's changing primary color in ant. But no luck with $ sign
    config.module
      .rule(["scss", "css"])
      .test(/\.scss$/)
      .issuer()
      .exclude.add([require.resolve("./src/styles/ant.less")])
      .end();
  }
}

@fajarsj
Copy link

fajarsj commented Apr 23, 2021

I did it on a clean project. create-react-app
And I was convinced that I connect ant.less once. No more. Any other ideas?

I know it's a too late but i just fixed this issue by deleting this line of code

On .babelrc or webpack.config.js delete this

["import", {
    "libraryName": "antd",
    "libraryDirectory": "es" //or "lib" for default
    //No "style" setting
}]

and dont forget to import 'ant.less';, on your project

@BoringDays
Copy link

How can I make this work in vue.config.js file? (it's using webpack-chain)
I tried config below but can't understand why it's not working.

// vue.config.js

module.exports = {
  // this one not working with ant.less file. If I removed issuer, include and end lines,
  // It's working for every scss related sections even in .vue files
  chainWebpack: config => {
    config.module
      .rule("less2")
      .issuer()
      .include.add([require.resolve("./src/styles/ant.less")])
      .end()
      .test(/\.scss$/)
      .use("./sassVarsToLess.js")
      .loader("./sassVarsToLess.js")
      .end();

    // this is working I guess, because when I use @primary-color: red; in variables.scss file, 
    // it's changing primary color in ant. But no luck with $ sign
    config.module
      .rule(["scss", "css"])
      .test(/\.scss$/)
      .issuer()
      .exclude.add([require.resolve("./src/styles/ant.less")])
      .end();
  }
}

Did you discover any solutions?

@sametsafak
Copy link

@BoringDays no i quit the job

@yairvol
Copy link

yairvol commented Mar 15, 2022

How can I make this work in vue.config.js file? (it's using webpack-chain)
I tried config below but can't understand why it's not working.

// vue.config.js

module.exports = {
  // this one not working with ant.less file. If I removed issuer, include and end lines,
  // It's working for every scss related sections even in .vue files
  chainWebpack: config => {
    config.module
      .rule("less2")
      .issuer()
      .include.add([require.resolve("./src/styles/ant.less")])
      .end()
      .test(/\.scss$/)
      .use("./sassVarsToLess.js")
      .loader("./sassVarsToLess.js")
      .end();

    // this is working I guess, because when I use @primary-color: red; in variables.scss file, 
    // it's changing primary color in ant. But no luck with $ sign
    config.module
      .rule(["scss", "css"])
      .test(/\.scss$/)
      .issuer()
      .exclude.add([require.resolve("./src/styles/ant.less")])
      .end();
  }
}

Did you discover any solutions?

@BoringDays
Did you?

@BoringDays
Copy link

How can I make this work in vue.config.js file? (it's using webpack-chain)
I tried config below but can't understand why it's not working.

// vue.config.js

module.exports = {
  // this one not working with ant.less file. If I removed issuer, include and end lines,
  // It's working for every scss related sections even in .vue files
  chainWebpack: config => {
    config.module
      .rule("less2")
      .issuer()
      .include.add([require.resolve("./src/styles/ant.less")])
      .end()
      .test(/\.scss$/)
      .use("./sassVarsToLess.js")
      .loader("./sassVarsToLess.js")
      .end();

    // this is working I guess, because when I use @primary-color: red; in variables.scss file, 
    // it's changing primary color in ant. But no luck with $ sign
    config.module
      .rule(["scss", "css"])
      .test(/\.scss$/)
      .issuer()
      .exclude.add([require.resolve("./src/styles/ant.less")])
      .end();
  }
}

Did you discover any solutions?

@BoringDays Did you?

No, I quited the job too, now I am forced to use angular 6, missing react and vue

@yairvol
Copy link

yairvol commented May 8, 2022

For others struggling with this - you can use .merge to add rules in the native (not vue.config) way.
See here

@amitrahav
Copy link

amitrahav commented May 11, 2022

Hey all, I followed your suggestions for importing scss via scssToLess custom loader, But I ran into problem - The custom loader does not run for the imported scss file from less.

ERROR in ./src/app/scss/ant.less (./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[1].oneOf[6].use[1]!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[1].oneOf[6].use[2]!./node_modules/resolve-url-loader/index.js??ruleSet[1].rules[1].oneOf[6].use[3]!./node_modules/less-loader/dist/cjs.js??ruleSet[1].rules[1].oneOf[6].use[4]!./src/app/scss/ant.less)
Module build failed (from ./node_modules/less-loader/dist/cjs.js):

I'm using:

  • "webpack": "^5.72.0"
  • "less": "^4.1.2"
  • "less-loader": "^10.2.0"
  • "sass": "^1.50.1",
  • "sass-loader": "^12.6.0"

Here is the relevant module section of my webpack config (after ejecting):

module: {
      strictExportPresence: true,
      rules: [
        // Handle node_modules packages that contain sourcemaps
        shouldUseSourceMap && {
          enforce: 'pre',
          exclude: /@babel(?:\/|\\{1,2})runtime/,
          test: /\.(js|mjs|jsx|ts|tsx|css)$/,
          loader: require.resolve('source-map-loader'),
        },
        {
          // "oneOf" will traverse all following loaders until one will
          // match the requirements. When no loader matches it will fall
          // back to the "file" loader at the end of the loader list.
          oneOf: [
            // TODO: Merge this config once `image/avif` is in the mime-db
            // https://github.com/jshttp/mime-db
            {
              test: [/\.avif$/],
              type: 'asset',
              mimetype: 'image/avif',
              parser: {
                dataUrlCondition: {
                  maxSize: imageInlineSizeLimit,
                },
              },
            },
            // "url" loader works like "file" loader except that it embeds assets
            // smaller than specified limit in bytes as data URLs to avoid requests.
            // A missing `test` is equivalent to a match.
            {
              test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
              type: 'asset',
              parser: {
                dataUrlCondition: {
                  maxSize: imageInlineSizeLimit,
                },
              },
            },
            {
              test: /\.svg$/,
              use: [
                {
                  loader: require.resolve('@svgr/webpack'),
                  options: {
                    prettier: false,
                    svgo: false,
                    svgoConfig: {
                      plugins: [{ removeViewBox: false }],
                    },
                    titleProp: true,
                    ref: true,
                  },
                },
                {
                  loader: require.resolve('file-loader'),
                  options: {
                    name: 'static/media/[name].[hash].[ext]',
                  },
                },
              ],
              issuer: {
                and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
              },
            },
            // Custome formatJs transformer
            {
              test: /\.tsx$/,
              use: [
                {
                  loader: 'ts-loader',
                  options: {
                    getCustomTransformers() {
                      return {
                        before: [
                          transform({
                            extractFromFormatMessageCall: true,
                            overrideIdFn: '[sha512:contenthash:base64:6]',
                            removeDefaultMessage: false,
                            ast: true
                          }),
                        ],
                      }
                    },
                  },
                },
              ],
            },
            // Process application JS with Babel.
            // The preset includes JSX, Flow, TypeScript, and some ESnext features.
            {
              test: /\.(js|mjs|jsx|ts|tsx)$/,
              include: paths.appSrc,
              loader: require.resolve('babel-loader'),
              options: {
                customize: require.resolve(
                  'babel-preset-react-app/webpack-overrides'
                ),
                presets: [
                  [
                    require.resolve('babel-preset-react-app'),
                    {
                      runtime: hasJsxRuntime ? 'automatic' : 'classic',
                    },
                  ],
                  '@babel/preset-typescript'
                ],
                
                plugins: [
                  isEnvDevelopment &&
                    shouldUseReactRefresh &&
                  require.resolve('react-refresh/babel'),
                   ['import', { 
                    "libraryName": "antd",
                    "libraryDirectory": "es" // or "lib" for default
                    // No "style" setting
                  }],
                ].filter(Boolean),
                // This is a feature of `babel-loader` for webpack (not Babel itself).
                // It enables caching results in ./node_modules/.cache/babel-loader/
                // directory for faster rebuilds.
                cacheDirectory: false,
                // See #6846 for context on why cacheCompression is disabled
                cacheCompression: false,
                compact: isEnvProduction,
              },
            },
            // Process any JS outside of the app with Babel.
            // Unlike the application JS, we only compile the standard ES features.
            {
              test: /\.(js|mjs)$/,
              exclude: /@babel(?:\/|\\{1,2})runtime/,
              loader: require.resolve('babel-loader'),
              options: {
                babelrc: false,
                configFile: false,
                compact: false,
                presets: [
                  [
                    require.resolve('babel-preset-react-app/dependencies'),
                    { helpers: true },
                  ],
                ],
                cacheDirectory: true,
                // See #6846 for context on why cacheCompression is disabled
                cacheCompression: false,
                
                // Babel sourcemaps are needed for debugging into node_modules
                // code.  Without the options below, debuggers like VSCode
                // show incorrect code and set breakpoints on the wrong lines.
                sourceMaps: shouldUseSourceMap,
                inputSourceMap: shouldUseSourceMap,
              },
            },
            // Include less-loader (exact settings may deviate depending on your building/bundling procedure)
            {
              test: lessRegex,
              use: getStyleLoaders(
                {
                  importLoaders: 3,
                  sourceMap: isEnvProduction && shouldUseSourceMap,
                  lessOptions: {
                    javascriptEnabled: true,
                  },
                },
                "less-loader"
              ),
              // Don't consider CSS imports dead code even if the
              // containing package claims to have no side effects.
              // Remove this when webpack adds a warning or an error for this.
              // See https://github.com/webpack/webpack/issues/6571
              sideEffects: true,
            },
            // "postcss" loader applies autoprefixer to our CSS.
            // "css" loader resolves paths in CSS and adds assets as dependencies.
            // "style" loader turns CSS into JS modules that inject <style> tags.
            // In production, we use MiniCSSExtractPlugin to extract that CSS
            // to a file, but in development "style" loader enables hot editing
            // of CSS.
            // By default we support CSS Modules with the extension .module.css
            {
              test: cssRegex,
              exclude: cssModuleRegex,
              use: getStyleLoaders({
                importLoaders: 1,
                sourceMap: isEnvProduction
                  ? shouldUseSourceMap
                  : isEnvDevelopment,
                modules: {
                  mode: 'icss',
                },
              }),
              // Don't consider CSS imports dead code even if the
              // containing package claims to have no side effects.
              // Remove this when webpack adds a warning or an error for this.
              // See https://github.com/webpack/webpack/issues/6571
              sideEffects: true,
            },
            // Adds support for CSS Modules (https://github.com/css-modules/css-modules)
            // using the extension .module.css
            {
              test: cssModuleRegex,
              use: getStyleLoaders({
                importLoaders: 1,
                sourceMap: isEnvProduction
                  ? shouldUseSourceMap
                  : isEnvDevelopment,
                modules: {
                  mode: 'local',
                  getLocalIdent: getCSSModuleLocalIdent,
                },
              }),
            },
            // Opt-in support for SASS (using .scss or .sass extensions).
            // By default we support SASS Modules with the
            // extensions .module.scss or .module.sass
            {
              test: sassRegex,
              exclude: [sassModuleRegex, lessRegex],
              use: getStyleLoaders(
                {
                  importLoaders: 3,
                  sourceMap: isEnvProduction
                    ? shouldUseSourceMap
                    : isEnvDevelopment,
                  modules: {
                    mode: 'icss',
                  },
                },
                'sass-loader'
              ),
              // Don't consider CSS imports dead code even if the
              // containing package claims to have no side effects.
              // Remove this when webpack adds a warning or an error for this.
              // See https://github.com/webpack/webpack/issues/6571
              sideEffects: true,
            },
            // Adds support for CSS Modules, but using SASS
            // using the extension .module.scss or .module.sass
            {
              test: sassModuleRegex,
              exclude: lessRegex,
              use: getStyleLoaders(
                {
                  importLoaders: 3,
                  sourceMap: isEnvProduction
                    ? shouldUseSourceMap
                    : isEnvDevelopment,
                  modules: {
                    mode: 'local',
                    getLocalIdent: getCSSModuleLocalIdent,
                  },
                },
                'sass-loader'
              ),
            },
            // "file" loader makes sure those assets get served by WebpackDevServer.
            // When you `import` an asset, you get its (virtual) filename.
            // In production, they would get copied to the `build` folder.
            // This loader doesn't use a "test" so it will catch all modules
            // that fall through the other loaders.
            {
              // Exclude `js` files to keep "css" loader working as it injects
              // its runtime that would otherwise be processed through "file" loader.
              // Also exclude `html` and `json` extensions so they get processed
              // by webpacks internal loaders.
              exclude: [/^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
              type: 'asset/resource',
            },
            // ** STOP ** Are you adding a new loader?
            // Make sure to add the new loader(s) before the "file" loader.
          ],
        },
        // Define a second rule for only being used from less files
        // This rule will only be used for converting our sass-variables to less-variables
        {
          test: sassRegex,
          issuer: lessRegex,
          use: {
            loader: path.resolve(
              paths.appSrc,
              'app/scss/sassVarsToLess.js' // I ensured it is the correct path
            ),
          }
        },
      ].filter(Boolean),
    }

'app/scss/sassVarsToLess.js' content is exactly like the gist above.

Got any ideas of how can I import scss files from less using webpack?

@yairvol
Copy link

yairvol commented May 11, 2022

In the end what I did was running a script beforehand, in the pre-build phase using package.json

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment