DEV Community

Cover image for Avoiding CSS order of appearance problems with CSS Modules
Andreas Riedmüller
Andreas Riedmüller

Posted on

Avoiding CSS order of appearance problems with CSS Modules

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

However, the reality looks more like this:

<button class="my-fancy-button blue">Send Hello</button>
Enter fullscreen mode Exit fullscreen mode

.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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>
  )
}

Enter fullscreen mode Exit fullscreen mode

The file helpers.module.css defines the two utility classes blue and green:

.blue {
  color: blue;
}

.green {
  color: green;
}
Enter fullscreen mode Exit fullscreen mode

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 (3)

Collapse
 
alohci profile image
Nicholas Stimpson

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.

Collapse
 
receter profile image
Andreas Riedmüller

Hi Nicholas,

thanks again to point this out! I have started to work on an (still experimental) component library and I do use CSS Layers on a library level, this works very nicely. github.com/receter/sys42/tree/main...

Collapse
 
receter profile image
Andreas Riedmüller

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.