🎨 👉 Start doing THIS to improve your CSS Architecture
Juan Otálora
Posted on September 3, 2023
In frontend development, there are two types of people: those who enjoy creating layouts and those who use Vanilla CSS. Just kidding. Vanilla CSS can have significant advantages, such as having complete control over your styles and understanding the purpose of each element. But let's be honest: even though the spec keeps improving with each version, using CSS can sometimes be a bit tedious, especially in the beginning when we're not entirely sure what each prop is for, and of course, unexpected results can happen.
Today, I'm here to talk to you about one of the best decisions I've made regarding CSS, which I now incorporate into all my projects (unless I opt for using a library like Tailwind). I'm talking about CSS Modules.
/* style.module.css */
.saveButton {
color: green;
}
// form.jsx
import styles from "./styles.module.css"
export const Form = () => {
// ...
return (
<form onSubmit={handleOnSubmit}>
<input name="email" />
<button className={styles.saveButton} />
</form>
)
}
Collisions between ClassNames
If you've used BEM or some class naming convention to improve your CSS architecture, you've probably noticed that as your codebase grows, it becomes easier for these class names to collide with each other. Sometimes, it can even happen with a component library you have installed.
This is chaotic. Moreover, thinking of names for classes and their various derivatives can be a tedious job, even though the component's name and its properties can help with this.
So, what does CSS Modules do to help us with this?
CSS Modules or How to Isolate Your Styles
Using CSS Modules accomplishes three main things:
- 👉 Avoiding conflicts between class names
- 👉 Easy and quick refactoring
- 👉 Avoiding the use of anti-patterns like global scope
Let's delve into each of these in more detail:
Avoiding Collisions
By definition, styles in CSS Modules are scoped, meaning they only affect the element to which they are applied. Therefore, even if there are multiple styles that refer to the same element in your project, it will only be applied to the one that has been explicitly referenced.
For example:
/* style.module.css */
.title {
font-weight: bolder;
}
// header.jsx
import styles from "./styles.module.css"
export const Header = () => {
// ...
return (
<div>
<h1 className={styles.title}>Hello world!</h1> {/* ✅ Style applied */}
<h2 className="title">This is my first post</h2> {/* ❌ Style NOT applied */}
</div>
)
}
The importance of all this lies in how this code is processed and how these class names appear when the application runs in the browser. Let's look at an example of what Next.JS does, although the result may depend on the framework you use or the configuration you set up:
👨💻 Input:
/* ProgressBar.module.css */
.background, .bar {
height: 4px;
border-radius: 999px;
}
.background {
background-color: var(--color-secondary);
width: 100%;
}
.bar {
background-color: var(--color-primary);
}
// ProgressBar.tsx
import styles from "./ProgressBar.module.css";
interface Props {
percentage: number;
}
export const ProgressBar = ({ percentage }: Props) => {
return (
<div className={styles.background}>
<div className={styles.bar} style={{ width: `${percentage}%` }} />
</div>
);
};
🖥️ Output:
<div class="ProgressBar_background__v_qdp">
<div class="ProgressBar_bar__YFKLn" style="width: 50%;"></div>
</div>
As you can imagine, what CSS Modules does in one way or another to achieve style scoping is to add a hash to the classes. In this case, to make it easier to read and debug the application, the module name appears before the hash, followed by the class name: module_class__hash
.
But as mentioned earlier, this is the example for Next.JS; it will work differently with other frameworks or configurations.
If you found this helpful or enjoyable, add a reaction! ❤️ Your likes are appreciated and keep me motivated!
Easy Refactoring
Since we treat CSS files as modules and class names as variables exported by these modules, renaming classes or identifying unused ones is easier than ever.
However, a word of caution! ⚠️ It's essential that even though class name collisions may not occur, the naming of classes should still follow a convention. While BEM may no longer provide much value, it's important for your team to establish a naming convention that works for you. CSS Modules won't solve naming issues.
For example, what if we want to change our Button
component to ActionButton
? Let's first look at an example with BEM:
/* (BEFORE) Button.css */
.Button_label {
text-transform: uppercase;
}
/* (AFTER) ActionButton.css */
.ActionButton_label {
text-transform: uppercase;
}
// (BEFORE) Button.tsx
import "./Button.css";
export const Button = ({ label }) => {
return (
<button>
<span className="Button_label">{label}</span>
</button>
);
};
// (AFTER) ActionButton.tsx
import "./ActionButton.css";
import Icon from "icons"
export const ActionButton = ({ label }) => {
return (
<button>
<Icon />
<span className="ActionButton_label">{label}</span>
</button>
);
};
As you can see, changing the component name isn't straightforward because you have to change all the class names (and we're lucky there's only one in this case). Even though modern IDEs have powerful find-and-replace tools, it's always easier when class names aren't coupled to the component name.
Now, let's look at an example using CSS Modules:
/* (BEFORE) Button.module.css */
.label {
text-transform: uppercase;
}
/* (AFTER) ActionButton.module.css */
.label {
text-transform: uppercase;
}
// (BEFORE) Button.tsx
import styles from "./Button.module.css";
export const Button = ({ label }) => {
return (
<button>
<span className={style.label}>{label}</span>
</button>
);
};
// (AFTER) ActionButton.tsx
import styles from "./ActionButton.module.css";
import Icon from "icons"
export const ActionButton = ({ label }) => {
return (
<button>
<Icon />
<span className={style.label}>{label}</span>
</button>
);
};
As you can see, by not referencing component names, they are more decoupled, and it's easier to make changes like this.
Avoiding Anti-Patterns
While CSS doesn't prevent poor CSS architecture, it does help with following some best practices like componentizing your projects or avoiding global styles.
Let's talk about componentization, a practice that became tremendously popular with the rise of web frameworks and libraries like Angular, React, or Vue. It's crucial for achieving reusability in your application (although, like everything, you need to determine the limit of how much you refactor).
CSS Modules motivate us to have a style file for each component, with these styles always going hand in hand with changes in component naming. In other words, it doesn't matter if the component goes from being called Button
to ActionButton
; the styles are decoupled as they are scoped.
Getting Started with CSS Modules 🚀
Unlike Vanilla CSS, CSS Modules may require some preparation before use. It depends on your bundler, pre-processors, and whether you're using TypeScript in your project or not:
- Webpack: This is one of the cases that requires more documentation, but fortunately, you'll find several boilerplates like this one.
-
Next.JS: The trendy web framework has native support for CSS Modules. You just need to start creating
.module.css
files. - Vite: CSS Modules are also natively compatible with Vite, so you won't have to do anything special to start using them.
I'd love to hear your thoughts on this! What do you think? Feel free to drop a comment below and share your perspective with me! 💬
Posted on September 3, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.