When I started web development, I wrote pure CSS, then I moved on to BEM (Block, Element, Modifier) methodology and LESS/SASS, and eventually adopted PostCSS and CSS Modules when I started using modern JavaScript frameworks and bundlers.
For me, CSS Modules primarily solved the inconvenience of managing unwieldy long class names to avoid global namespace pollution and accidental style overrides. And despite all the new CSS libraries popping up lately, I still love the simplicity of using CSS modules.
In this article I want to shed light on one challenge I've encountered when using CSS Modules:
Position and order of appearance
In CSS (Cascading Style Sheets) the order in which rules appear affects the final styling of elements. Later-defined CSS rules have higher precedence and can override earlier ones. This is one of four stages in the cascade algorithm, the others are specificity, origin, and importance.
Sometimes, the order of CSS rules can create confusing scenarios. Imagine you have a div
and apply multiple classes that set color
like this:
<div class="red blue">Hello World</div>
What color will "Hello World" have? To be able to answer this question with certainty, you need to know the order in which the classes are defined.
In this case, the obvious solution is to use only one class:
<div class="red">Hello World</div>
However, the reality looks more like this:
<button class="my-fancy-button blue">Send Hello</button>
.my-fancy-button
is a class that defines the appearance of a fancy button, and .blue
is a utility class to set the color blue in this context. For this to work, you will have to to make sure that .blue
is defined later.
Bundlers
This next example illustrates an order of appearance problem you may encounter when using CSS Modules in any bundler setup.
Let's say, you create a React component and style it using a custom class. In addition the component is designed to accept a className
prop that can be used to pass an additional class from outside.
import { button } from "styles.module.css";
function Button(props) {
return (
<button className={concatClassNames(button, props.className)}>
{props.label}
</button>
);
}
Subsequently, you use this component, utilizing a helper class to override specific styles.
import { Button } from "components/Button";
import { blue } from "../../helpers.module.css";
function BlueButton() {
return (
<div>
<Button className={blue} label="Click Me" />
</div>
);
}
This will work like expected because bundlers typically sequence generated/imported CSS based on the first occurrence. You can think about it like a global style sheet; whenever CSS is imported, it's appended to a list of style rules.
blue
is imported after Button
, so it is added later and overwrites the color defined by the local class button
imported inside Button
The import order is crucial here; if you would import blue
before Button
, the utility class wouldn't be able to override the color anymore:
// If the utility class is imported earlier, it doesn't
// have the ability to override styles from Button.
import { blue } from "../../helpers.module.css";
import { Button } from "components/Button";
Ok, so we just need to make sure to always import such utility classes last, right?
In general this is true, but it might be more difficult than you expect, hang on.
The hidden first occurrence
Let’s assume you import BlueButton
and also the same helper class blue
in another file:
import { BlueButton } from "components/BlueButton";
import { MyHeading } from "components/MyHeading";
…
// The utility class is imported last (in this file)
import { blue } from "../../helpers.module.css";
function MyCoolInterface(props) {
return (
<div>
<MyHeading className={blue}>
Click the blue button:
</MyHeading>
<BlueButton />
…
</div>
);
}
Despite blue
being imported last in this file, it's first occurrence is inside BlueButton
. This means that the class blue
will be added ahead of any CSS imported after BlueButton
. In this example the class blue
won't have precedence to override styles defined in MyHeading
.
While in this particular case you could theoretically move the MyHeading
import to the top, there are situations where reordering imports merely transfers the problem to another location.
Avoiding hidden first occurrences
An effective solution to avoid this problem would be a steadfast rule: always import CSS in an application only once, and never in multiple locations.
By following this rule, you ensure that each import is the first occurrence, and you can stay confident about the sequence. For each file where this rule is not followed, expect that the rules might spontaneously receive lower precedence than they currently have in your context.
To use classes in multiple locations you can still import the styles once at a higher level, such as your main file or an App
component. Then share the classes by passing them down to children.
In React, you could do this via the Context API:
import App from "./App"
import * as helperStyles from "helpers.module.css"
<MyContext.Provider value={{ helperStyles }}>
<App />
</MyContext.Provider>
But you could still run into order of appearance issues.
Imagine you create a component MyHeading
that uses a class provided by MyContext
and in addition accepts a className
from outside:
function MyHeading({children, className}) {
const { helperStyles } = useContext(MyContext);
return (
<h1
// This can get you into trouble
className={concatClassNames(helperStyles.green, className)}
>{props.children}</h1>
)
}
The file helpers.module.css
defines the two utility classes blue
and green
:
.blue {
color: blue;
}
.green {
color: green;
}
When you subsequently use MyHeading
and pass it the blue
class obtained from MyContext
like <MyHeading className={blue} />
, the blue
class and the red
class would both be applied. And blue
won't have sufficient precedence to override the color because it is defined first in helpers.module.css
.
I do like the pattern of concatenating class names across multiple levels in the component tree. But to keep this pattern intuitive you need to make sure that the precedence of rules resembles the hierarchy in your components.
You can accomplish this by adhering to the aforementioned rule and avoiding the combination of shared class names at multiple levels.
Conclusion
While CSS Modules and similar tools offer elegant solutions to avoid namespace conflicts, managing the precedence of style rules is still relevant.
Thank you for reading! Let me know if you found it worthwhile reading and I hope this article has piqued your curiosity. Feel free to share your thoughts and experiences on this topic in the comments below.
Top comments (2)
My first thought was whether some of these issues could be addressed using Cascade Layers. You put your component styles into one layer and your helper styles into another and declare the layers in that order before you start importing stylesheets. Then you can import your stylesheets with impunity safe in the knowledge that the helper styles will always win over the component styles.
Hi Nicholas,
Thanks a lot for your comment. I just read w3.org/TR/css-cascade-5/#layering and am very glad you pointed it out to me. I am looking forward to try using Cascade Layers together with CSS Modules to address theses issues.