What are CSS Modules and how to use it in React

What are CSS Modules and how to use it in React

CSS Modules are "CSS files in which all class names and animation names are scoped locally by default". Instead of having CSS files and classes that are static, CSS Modules creates a dynamic implementation that is locally scoped to the HTML with the help of Webpack or Browserify.

CSS Modules is popular in React because of the following reasons:

  1. Scoped: CSS Modules are scoped when you use them the right way.
  2. Highly composable: You can compose different styles in a lot of ways.
  3. Tree shakable: Styles you don't use are removed, just like modern JS tooling.
  4. Very easy to pick up: CSS Modules are very easy to understand and take almost no effort to start using.
  5. It's just CSS: CSS Modules are just CSS that you write with some specific rules in mind. You get great tooling from your IDE, and IDEs are great at plain ol' CSS.

CSS Modules work seamlessly with pre-processors like SASS, LESS, and Stylus. It also works with PostCSS.

What is PostCSS?

PostCSS is CSS converted for CSS Modules, which gets rid of global CSS classes and ids. This reduces naming conflicts and global space pollution as your project grows.

How to use CSS Modules in React

Let's start with an example. We're going to build a Card component. This will be our directory structure:

 |- Card.jsx
 |- Card.module.css

We're naming our CSS file a *.module.css because it is a convention. It is also how build tools know that it is a CSS Module file.

Now let's look at the CSS file. We used specifically camel casing for our css class so that it matches our JavaScript casing.

 /* Card.module.scss */
 .container {}
 .avatar {}
 .userInfo {}
 .userName {}
 .userStatus {}

Under normal circumstances and for plain CSS, we'd just use our CSS classes with className. For example - <div className="userStatus">...</div>. However, for CSS Modules, we surround our CSS with {} . So the React code looks something like this: <div className={css.userStatus}>...</div>

Here is an example with the imported './Card.module.scss' CSS Module:

 import css from './Card.module.scss';
 
 export const Card = () => {
   return (
     <section className={css.container}>
       <img className={css.avatar} src="..." />
       <div className={css.userInfo}>
         <div className={css.userName}>...</div>
         <div className={css.userStatus}>...</div>
       </div>
     </section>
   );
 };

Instead of regular static strings, we're using css.* values. This is because the CSS classes are being used like JavaScript modules. It is also a reason why we use camel casing instead of dashes in our CSS.

How CSS Modules work under the hood

When you import a CSS module file, it looks something like this:

 export const container = '_container_bslv0_1';
 export const avatar = '_avatar_bslv0_8';
 export const userInfo = '_userInfo_bslv0_14';
 export const userName = '_userName_bslv0_21';
 export const userStatus = '_userStatus_bslv0_40';
 export default {
   container: container,
   avatar: avatar,
   userInfo: userInfo,
   userName: userName,
   userStatus: userStatus,
 };

The build tools transforms our class names into variables that are exported separately as named exports and as an object. Another CSS file gets generated and then mapped to the associated const . As a result, your CSS is specifically scoped to the space it is used. Here is an example:

 ._container_bslv0_1 {}
 ._avatar_bslv0_8 {}
 ._userInfo_bslv0_14 {}
 ._userName_bslv0_21 {}
 ._userStatus_bslv0_40 {}

It is good to note that by default CSS Modules are scoped only to classes. This means that #id and global selectors are not processed by CSS Modules. However, descendants of classes are scoped. For example .container img will be scoped by CSS Modules but img on its own will not.

#CSS Modules features and how to use them

Besides scoping, CSS Modules also lets you:

  • create global styling
  • compose multiple styles together
  • create variables

Create global style in React with CSS Module via :global

:global is a way to create global selectors with CSS Modules. Here is an example of SCSS code with the :global flag.

 .container {
   .someRandomDiv {
     .header {
       color: green;
     }
 
     :global(body.dark) .header {
       color: blue;
     }
   }
 }

Composing multiple styles together

In CSS Modules, you can create a class and then compose it within other classes by using the composes selector. Here is an example:

 .classA {
   background-color: green;
   color: white;
 }
 
 .classB {
   composes: classA;
   color: blue;
 }

The classes being called in by compose do not have to be in the same file. It can be imported in. For example:

 .classB {
   composes: classA from './classB.css';
   color: blue;
 }

The composes selector allows us to build highly reusable styles, resulting in easier orchestration of design systems.

CSS variable systems with CSS Modules

CSS Modules have a variable system that allows you to efficiently create DRY values for your CSS.

 @value blue: #0c77f8;
 @value red: #ff0000;
 @value green: #aaf200;
 
 .button {
   color: blue;
   display: inline-block;
 }

In the example above, we've redefined the browser defaults for blue, red , and green. For cleaner referencing and reusability, these values can be defined in a separate file and imported in.

 /* import your colors... */
 @value colors: "./colors.css";
 @value blue, red, green from colors;
 
 .button {
   color: blue;
   display: inline-block;
 }

#Visual Studio Code import error for CSS Module

CSS Modules are great but if you are using Visual Studio Code, Intellisense can create issues. For example, although technically correct, if you are working with TypeScript, the following will create an error:

 import css from './Card.module.css';

This is because VSCode will import it as a css file and not as a CSS Module file. There are two ways to fix it:

  • edit your types.d.ts file
  • or, install typescript-plugin-css-modules

Note: If you're using create-react-app or Vite's react scaffolds, you don't need the steps below, they already have the minimal configuration built-in to allow .css, .scss, .less, .styl, etc in import statements.

Fix 1: Editing types.d.ts

Create a types.d.ts in your src folder and put in the following configuration:

 declare module '*.module.css' {
   const classes: { [key: string]: string };
   export default classes;
 }
 
 declare module '*.module.scss' {
   const classes: { [key: string]: string };
   export default classes;
 }
 
 declare module '*.module.sass' {
   const classes: { [key: string]: string };
   export default classes;
 }
 
 declare module '*.module.less' {
   const classes: { [key: string]: string };
   export default classes;
 }
 
 declare module '*.module.styl' {
   const classes: { [key: string]: string };
   export default classes;
 }

This will tell VSCode to interpret your CSS Modules as key-pair values. This will fix your warnings with using css.* in your code because it now knows to return a string.

https://res.cloudinary.com/layercode/image/upload/v1628263501/css_modules_intellisense_demo_567ef36d33.gif

Fix 2: Use typescript-plugin-css-modules

Using typescript-plugin-css-modules is the quickest way to set up any TypeScript project with CSS Modules. Use the following commands to install typescript-plugin-css-modules.

 npm i --save-dev typescript-plugin-css-modules
 
 # Or if you're a yarn person
 
 yarn add --save-dev typescript-plugin-css-modules

Next, open up your tsconfig.json, and add the following to the compilerOptions property:

 "plugins": [{ "name": "typescript-plugin-css-modules" }]

Set VSCode TypeScript version to the one that's used on your project. You can view the version on the bottom left of your console.

https://res.cloudinary.com/layercode/image/upload/v1628263541/css_modules_typescript_version_photo_1_2c9fe63906.png

If you click on it, a list popup will open up and you can select your TypeScript version.

https://res.cloudinary.com/layercode/image/upload/v1628263548/css_modules_typescript_version_photo_2_f19d891815.png

https://res.cloudinary.com/layercode/image/upload/v1628263548/css_modules_typescript_version_photo_3_55b7f6251b.png

#Using clsx for cleaner CSS Modules conditionals

CSS Modules rely on using the css.* pattern, composing multiple classes can result in long strings. For example:

 <div className={`${css.classA} ${css.classB} ${css.classC}`} />

It looks a little ugly, but it's still manageable. However, once you start having conditional classes, things can get uglier. For example:

 <div className={`${css.classA} ${condition1 ? css.classB : ''} ${condition2 ? css.classC : ''}`} />

It's easy to fall into a ternary trap. We have to render an empty string manually if the condition is false. If we did just condition1 && css.classB, it would've been pretty clean and legible. But if the condition is false, it would output null as a class name.

To make this cleaner, we can use the clsx library. It's a small 228B library that makes dealing with class composition much easier.

Here is how you can use it for cleaner composition:

 <div
   className={clsx({
     [css.classA]: true,
     [css.classB]: condition1,
     [css.classC]: condition2,
   })}
 />

#Wrapping up

CSS Modules are one way to upgrade your CSS and make it more manageable in the long run. CSS scoping reduces the manual work required for matching CSS classes to the right context. It also reduces visual errors caused by global conflicts.

CSS Modules also encourage DRY patterns in your code. It is not exclusive to React and can also be used with other frontend libraries such as Angular and Vue. No one wants to use !important overrides in CSS because it's a slippery slope. However, !important becomes tempting as the project grows and there are multiple people working in a team. CSS Modules are one way to solve this issue.

Share this post