css module file and components should not be one-to-many


Problem

Recently, I encountered a problem regarding CSS modules. It is generally considered good practice to associate one CSS module file with a single file, like so:

Directory structure

- button
  - NormalButton.module.css
  - NormalButton.tsx
  - LinkButton.module.css
  - LinkButton.tsx

The reason behind this is that in CSS modules, if the specificity is the same, properties defined later will take precedence. This can change based on the order in which the properties are imported within the file. Therefore, if the import order is automatically rearranged by a formatter, or if the import order is unpredictable as with Next.js’s dynamic import, unexpected styles may be applied.

However, there are instances where you might want to share styles across multiple files. For example, both NormalButton and LinkButton may wrap a button and an a tag, respectively, and you might want to apply common styles to them. But adhering to the one CSS module file per file rule means you end up defining the same styles in multiple places, which violates the DRY (Don’t Repeat Yourself) principle.

Solution

The solution is to use the composes feature of CSS modules. The composes property allows you to inherit properties from other classes, and this can be done with classes from other files as well. Thus, you can use it as shown below:

Directory structure

- button
  - Action.module.css
  - NormalButton.module.css
  - NormalButton.tsx
  - LinkButton.module.css
  - LinkButton.tsx

Action.module.css

.action {
  background: red;
  padding: 4px;
}

NormalButton.module.css

.button {
  composes: action from './Action.module.css';
}

By doing this, you can use shared styles across multiple places in a DRY manner.

Note

composes is a unique feature of CSS modules, so you might see a warning in VSCode like this: waring in vscode

To correct this, you need to add the following to your VSCode’s settings.json:

{
  "css.lint.validProperties": ["composes"],
}