CSS Modules themselves are relatively simple, but the reason FOR writing this is that I’ve found many tutorials and practice articles on CSS Modules online are out of date, and there may be ways of using them that are not currently feasible. You can go to CSS-modules-demo to see the complete code for all the examples in this article.

features

CSS Modules is relatively simple, first introduce CSS Modules common features. CSS files ending in.module. CSS represent CSS Modules, and other.css files are just plain CSS Modules. This is also a convention in many projects.

Local/global scope

Since CSS rules apply globally, you must ensure that a class name (.class) is unique if you want it to have a “local scope” effect.

  • Using class selector syntax (.className) or:localA pseudo-class declares a class as a locally scoped class, and the class name is compiled into a unique hash class name (hash string, e.g_1wtd1y1DR22bj8P0JYV7nH). Note that the same local class name is used multiple times and the compiled class name is the same.
  • use:globalThe pseudo-class is declared as a global class name, which does not change after compilation.
/* local scope */
.title {
  color: red;
}
/* Equivalent to: :local(.title) {color: red; } * /

/* Global scope */
:global(.title) {
  color: green;
}
Copy the code

Use:

import React from 'react';
import styles from "./scope.module.css";

const Demo = () = > {
  return (
    <div>
      <h3 className={styles.title}>Local scope</h3>
      <h3 className="title">Global scope</h3>
    </div>)};export default Demo;
Copy the code

Custom hash class name

The default algorithm generates a unique but unreadable hash class name (e.g., title compilers _3ZYDE4L1yATCOkGN-dbwel), so we often want to customize the hash class name in two ways:

  1. Set up thecss-loader ηš„ options.modules.localIdentNameOption, the value accepts a string template. Such as:
module.exports = {
  module: {
    rules: [{test: /\.module\.css$/,
        use: [
          require.resolve('style-loader'),
          {
            loader: require.resolve('css-loader'),
            options: {
              // Enable CSS Modules
              modules: {
                compileType: 'module'.localIdentName: "[path][name]__[local]--[hash:base64:5]",},}}]},],},};Copy the code
  1. By setting thecss-loader δΈ­ options.modules.getLocalIdentOption, the value passes a custom function.

Using the Settings in create-react-app as an example πŸ‘‡, the getCSSModuleLocalIdent function is used to create a unique classname of the format [filename]_[classname]__[hash]. For example, the title class name in.content.module. CSS compiles to content_title__2eyf6.

const loaderUtils = require('loader-utils');
const path = require('path');

function getCSSModuleLocalIdent (context, localIdentName, localName, options) {
  // Use a filename or folder name
  const fileNameOrFolder = context.resourcePath.match(/index\.module\.(css|scss|sass)$/)?'[folder]' : '[name]';
  // Create hash based on file location and class name
  const hash = loaderUtils.getHashDigest(
    path.posix.relative(context.rootContext, context.resourcePath) + localName,
    'md5'.'base64'.5
  );
  // Use loaderUtils to find file or folder names
  const className = loaderUtils.interpolateName(
    context,
    fileNameOrFolder + '_' + localName + '__' + hash,
    options
  );
  // Remove '.module 'from the class name and replace all "." with "_".
  return className.replace('.module_'.'_').replace(/\./g.'_');
};

module.exports = {
  module: {
    rules: [{test: /\.module\.css$/,
        use: [
          require.resolve('style-loader'),
          {
            loader: require.resolve('css-loader'),
            options: {
              modules: {
                compileType: 'module'.getLocalIdent: getCSSModuleLocalIdent
              },
            }
          }
        ]
      },
    ],
  },
};
Copy the code

combination

Composes are used in CSS Modules to implement style reuse. Composes combine the names of classes in this Module as well as imported classes in other CSS Modules only in local styles (:local) :

/* btn.module.css */
.btn {
  border: 1px solid #ccc;
}

/* Combine the class names in this module πŸ‘‡ */
.btnPrimary {
  composes: btn;
  background: #1890ff;
}

/* Combine the names of classes imported from other CSS modules πŸ‘‡ */
.btnCircle {
  font-size: 14px;
  composes: circle from './shape.module.css';
  /* To import from multiple modules, use multiple comPoses */
  composes: bgColor color from "./color.module.css";
  composes: btn;
}
Copy the code
// Composes.js
import React from 'react';
import styles from "./btn.module.css";

const Demo = () = > {
  return (
    <div>
      <button className={styles.btn}>Button</button>
      <button className={styles.btnPrimary}>Primary Button</button>
      <button className={styles.btnCircle}><Icon type="search"/></button>
    </div>
  );
}
export default Demo;
Copy the code

ClassName ={styles.btnPrimary} The compiled class name is class=”btn_btnPrimary__1RVFt btn_btn__1QIyw “.

Composes in :global will report an error:

/* Error: composition is only allowed when selector is single :local class name not in ".button", ".button" is weird */
:global(.button) {
  composes: btn;
  color: red;
}
Copy the code

variable

The variables. The module. The CSS:

@value primary: #BF4040;
@value secondary: #1F4F7F;
Copy the code

The text. The module. The CSS:

@value fontSize: 16px;

/* Import πŸ‘‡ */ from other module files
@value primary as color-primary, secondary from "./variables.module.css";

/* is equivalent to πŸ‘‡ */
@value variables: "./variables.module.css";
@value primary as color-primary, secondary from variables;

/* value as selector name */
@value selectorValue: secondary-color;
.selectorValue {
  color: secondary;
}

.textPrimary {
  font-size: fontSize;
  color: color-primary;
}

.textSecondary {
  font-size: fontSize;
  color: secondary;
}
Copy the code
import React from 'react';
import styles from "./text.module.css";

const Demo = () = > {
  return (
    <div>Variables:<br/>
      <p className={styles.textPrimary}>This is a passage...</p>
      <p className={styles.textSecondary}>This is a passage...</p>
      <p className={styles['secondary-color']} >This is a passage...</p>
    </div>)}export default Demo;
Copy the code

Import and export variables

This feature is used to pass variables from CSS to JS. CSS Module implements this feature through Interoperable CSS (ICSS), which is the low-level file format specification for CSS Modules. ICSS adds two additional pseudo-selectors :import and :export to the standard CSS. Therefore, the CSS Modules feature described above at πŸ‘† cannot be used in ICSS.

In real projects, files with the convention extension.module. CSS are usually resolved to CSS Modules, while regular.css files are resolved to ICSS. CSS – loader via options.modules.com pileType option set CSS Modules parsing.

{
  test: /\.css$/.// Matches the CSS module
  exclude: /\.module\.css$/.// Exclude '.module. CSS 'extension files
  use: [
    require.resolve('style-loader'),
    {
      loader: require.resolve('css-loader'),
      options: {
        modules: {
          // compileType: controls compileType
          compileType: 'icss'.// Enable only :import and :export},},]}, {test: /\.module\.css$/.// Match CSS Modules
  use: [
    require.resolve('style-loader'),
    {
      loader: require.resolve('css-loader'),
      options: {
        modules: {
          compileType: 'module'.// CSS Modules All features},}},]},Copy the code

If set tofalseValues improve performance because CSS Modules features are avoided.

:export ε’Œ :import

/* Import variables */
:import("path/to/dep.css") {
  localAlias: keyFromDep;
  / *... * /
}
/* Export variable */
:export {
  exportedKey: exportedValue;
	/ *... * /
}
Copy the code

:export is equivalent to module.exports in CJS:

module.exports = {
	exportedKey: exportedValue;
}
Copy the code

Recommended but not mandatory:

  • :export: Only one:exportBlock, located at the top of the file, but in any:importAfter a block.
  • :importEach dependency should have one import; All imports should be at the top of the file; Local aliases should be double underlined (__) is a prefix.

Specific examples:

. / dep. CSS:

:export {
  theme-color: #1890ff;
  header-height: 60px;
  header-name: abc-header;
  color-secondary: # 666;
  screen-min: 768px;
  screen-max: 1200px;
}
Copy the code

Incorrect-site. CSS:

:import("./dep.css") {
  __themeColor: theme-color;
  __headerHeight: header-height;
  __headerName: header-name;
  __secondary: color-secondary;
  __screenMin: screen-min;
  __screenMax: screen-max;
}

/* Imported variables can be used in any selector, any value, and media query parameters */
/* Any selector */
.__headerName .logo {
  color: red;
  line-height: __headerHeight;
}

/* Any value */
.border-theme {
  border: 1px solid __themeColor;
}

/* Media query parameters */
@media (min-width: __screenMin) and (max-width: __screenMax) {
  .__headerName {
    box-shadow: 0 4px 4px__secondary; }}/* Export for js modules */
:export {
  headerHeight: __headerHeight;
  headerName: __headerName;
}
Copy the code

πŸ‘† because of the exported variables used above (__headerName,__headerHeight), so:exportBlock must be placed under the style rule, otherwise the style rule will be invalid.

Use:

import React from 'react';
import styles from "./icss.css";
const { headerHeight, headerName } = styles;

const Demo = () = > {
  return (
    <div className={` ${headerName} border-theme`} style={{ height: headerHeight}} >
      <span className="logo">logo</span>
    </div>)}export default Demo;
Copy the code

The project practice

In actual project development, webpack is used, CSS -loader is used to parse CSS Modules. The package.json and webpack.config.js configurations are as follows: πŸ‘‡. Use the NPM run dev command to start the local service.

Webpack configuration

Package. Json:

{
  "name": "css-modules-demo"."scripts": {
    "dev": "webpack serve --hot"."build": "webpack"
  },
  "devDependencies": {
    "@babel/core": "^ 7.14.6"."@babel/preset-react": "^ 7.14.5"."babel-loader": "^ 8.2.2"."css-loader": "^ 5.2.6." "."html-webpack-plugin": "^ 5.3.2." "."loader-utils": "^ 2.0.0." "."postcss-loader": "^ 6.1.1." "."style-loader": "^ 3.0.0"."webpack": "^ 5.44.0"."webpack-cli": "^ 4.7.2." "."webpack-dev-server": "^ 3.11.2"
  },
  "dependencies": {
    "react": "^ 17.0.2"."react-dom": "^ 17.0.2"
  },
  // ...
}
Copy the code

The following conventions are usually made in the project:

  • In order to.module.cssThe extension file at the end represents CSS Modules (parsing all CSS Modules features).
  • other.cssFiles are treated only as regular CSS modules (parsing ICSS features only, with additional support over the standard CSS specification:import ε’Œ :export).
import React from 'react';
import styles from './Button.module.css'; // Import the CSS Modules style file
import './another-stylesheet.css'; // Import regular styles

const Button = () = > {
  // as a js object reference
  return <button className={styles.error}>Error Button</button>;
}
Copy the code

The purpose of this convention is on the one hand to standardize code writing, on the other hand to avoid all style files webpack have to do CSS Modules parsing, resulting in performance waste.

You can only use interactive CSS features (ICSS features such as :import and :export) and no other CSS Module features by setting compileType for all files that don’t match the *.module. SCSS naming convention. This is for reference only, because before V4, CSS-Loader applied the ICSS feature to all files by default.

Therefore, do the following webpack configuration, where CSS modules is enabled for the modules option of CSS-loader. Modules.com pileType controls the compilation level, and modules and ICSS are optional. For details about csS-Loader configuration, see here.

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');

const cssModuleRegex = /\.module\.css$/;

module.exports = {
  mode: 'development'.devServer: {
    contentBase: './dist'.hot: true,},module: {
    rules: [{oneOf: [{test: /\.(js|mjs|jsx|ts|tsx)$/,
            loader: require.resolve('babel-loader'),
            options: {
              presets: ['@babel/preset-react']}},// Matches a common CSS module
          {
            test: /\.css$/,
            exclude: /\.module\.css$/.// Exclude files with the extension '.module. CSS '
            use: [
              require.resolve('style-loader'),
              {
                loader: require.resolve('css-loader'),
                options: {
                  modules: {
                    // Control the compile level
                    compileType: 'icss'.// Enable only :import and :export},}},]},// Match CSS Modules
          {
            test: /\.module\.css$/,
            use: [
              require.resolve('style-loader'),
              {
                loader: require.resolve('css-loader'),
                options: {
                  modules: {
                    compileType: 'module'.// CSS Modules All features
                    getLocalIdent: getCSSModuleLocalIdent, // Customize the hash class name, as described above at πŸ‘†},}},]},]},plugins: [
    new HtmlWebpackPlugin({
      inject: true.template: 'index.html'}})];Copy the code

sass/scss

Preprocessors like Less and SASS are commonly used in projects, and sASS is an example. It is also agreed that only.module. SCSS or.module.sass extensions parse CSS Modules features, and other.scss or.sass extension files are treated as normal Sass Modules. Only support ICSS features (:import and :export).

Modify webpack.config.js to add two matching groups. The main difference is the modules.compileType option:

// Match the normal SASS module, ICSS only
{
  test: /\.(scss|sass)$/,
  exclude: /\.module\.(scss|sass)$/.// Exclude.module. SCSS or.module.sass extension files
  use: [
    require.resolve('style-loader'),
    {
      loader: require.resolve('css-loader'),
      options: {
        importLoaders: 3.// 3 => postcss-loader, resolve-url-loader, sass-loader
        modules: {
          compileType: 'icss',},}}, {// Help sass-loader to find the corresponding URL
      loader: require.resolve('resolve-url-loader'),
      options: {
        root: resolveApp('src'),}}, {loader: require.resolve('sass-loader'),
      options: {
        sourceMap: true.// This is not necessary}},],},// CSS Modules are supported, matching only.module. SCSS or.module.sass extension files
{
  test: /\.module\.(scss|sass)$/,
  use: [
    require.resolve('style-loader'),
    {
      loader: require.resolve('css-loader'),
      options: {
        importLoaders: 3.modules: {
          compileType: 'module'.getLocalIdent: getCSSModuleLocalIdent,
        },
      }
    },
    {
      loader: require.resolve('resolve-url-loader'),
      options: {
        root: resolveApp('src'),}}, {loader: require.resolve('sass-loader'),
      options: {
        sourceMap: true,},},],},// The following example will use file-loader to process images
{
  loader: require.resolve('file-loader'),
  exclude: [/\.(js|jsx)$/./\.html$/./\.json$/].options: {
    name: 'static/media/[name].[hash:8].[ext]',}},Copy the code

Description:

  • importLoaders: indicates that the configuration is incss-loaderThere are several loaders that can be processed before@importResources (e.g.,@import 'a.css';). Above configuration3said@importIncoming resources can pass throughpostcss-loader,resolve-url-loader ε’Œ sass-loaderTo deal with.
  • resolve-url-loaderHelp:sass-loaderFind the corresponding URL resource. Saas does not provideThe url rewriteThis loader is set in the loader chainsass-loaderI could have rewritten the URL before.

Example:

Create header.module.sass and header.sass files respectively, with the following contents:

$shadow-color: #ccc
$header-height: 60px
$theme: #1890ff

.className
  border: 1px solid $theme
  padding: 20px
  box-shadow: 0 4px 4px $shadow-color
  a
    background-color: #fffDisplay: inline-block // Export height:$header-height
Copy the code
import React from 'react';
import styles1 from "./header.module.sass";
import styles2 from "./header.sass"; // ICSS
import logo from './logo.png';

console.log('styles1', styles1); // {height: "60px", className: "header_className__rUUph"}
console.log('styles2', styles2); // {height: "60px"}

const Logo = ({ height }) = > (
  <img style={{ height.width: height }} src={logo} />
);

const Demo = () = > {
  return (
    <div className={styles1.className}>
      <Logo height={styles1.height} />
    </div>
  );
};
export default Demo;
Copy the code

classnames

Matching classnames makes it easier to use:

import classNames from 'classnames';
import styles from './dialog.module.css';

const Dialog = ({ disabled }) = > {
  constcx = classNames({ [styles.confirm]: ! disabled, [styles.disabledConfirm]: disabled });return (
    <div className={styles.root}>
      <a className={cx}>Confirm</a>.</div>
  );
};
Copy the code

reference

  • css-loader#modules
  • CSS Modules
  • Exporting values variables
  • CSS Modules ICSS
  • Create React App