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
}
},
// ...
]
}
@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