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:
To correct this, you need to add the following to your VSCode’s settings.json:
{
"css.lint.validProperties": ["composes"],
}