CSS Modules (Part 2): Composition and Global Styles #28
📖 Introduction
In the previous article, we discovered how CSS Modules solve the problem of global scope by making styles local by default. This is a huge step forward, but what if we want to share styles between classes or intentionally create a global style?
This article dives into the more advanced features of CSS Modules. We'll explore composition, a powerful feature for inheriting and combining styles, and learn how to escape the local scope when you truly need a global class.
📚 Prerequisites
Before we begin, please ensure you have a solid grasp of the following concepts:
- CSS Modules Basics: You must understand how to create and use a
.module.css
file and apply its styles in a React component. - CSS Principles: A basic understanding of CSS inheritance and specificity.
🎯 Article Outline: What You'll Master
In this article, you will learn:
- ✅ The
composes
Keyword: How to share styles between classes within the same file. - ✅ Composition from Other Files: How to inherit styles from a different CSS Module.
- ✅ Applying Multiple Classes: Techniques for combining several composed and local classes on a single element.
- ✅ Escaping the Local Scope: How to use the
:global
keyword to create a standard, global CSS class from within a CSS Module.
🧠 Section 1: The Core Concept: Style Composition
A core principle of good CSS is to keep it DRY (Don't Repeat Yourself). Composition is the feature in CSS Modules that allows you to achieve this. It lets one class inherit all the styles from another class.
The composes
keyword is the key to this feature. It tells the CSS Modules processor that a class should include all the styles from one or more other classes.
💻 Section 2: Composition in Practice
Let's explore the different ways to use composes
.
2.1 - Composition Within the Same File
This is useful for creating variations of a base component style. Imagine a base .btn
class and several color variations.
Button.module.css
/* Define a base class */
.btn {
padding: 10px 15px;
border: none;
border-radius: 5px;
font-size: 1rem;
}
/* Create a variation that composes the base class */
.primary {
composes: btn; /* Inherits all styles from .btn */
background-color: blue;
color: white;
}
/* Create another variation */
.secondary {
composes: btn; /* Also inherits all styles from .btn */
background-color: grey;
color: white;
}
Button.jsx
import React from 'react';
import styles from './Button.module.css';
function Button() {
return (
<div>
<button className={styles.primary}>Primary</button>
<button className={styles.secondary}>Secondary</button>
</div>
);
}
How it Works:
When you use className={styles.primary}
, the rendered HTML element will have two classes applied: the unique generated class for .btn
and the unique generated class for .primary
. The composes
rule effectively merges the styles, with the properties in the local class (.primary
) overriding any inherited properties if they conflict.
2.2 - Composition from Another File
This is powerful for creating a shared library of utility styles. Let's create a separate file for common layout styles.
utils.module.css
.flexCenter {
display: flex;
justify-content: center;
align-items: center;
}
Card.module.css
.card {
/* Inherit styles from another file */
composes: flexCenter from './utils.module.css';
padding: 20px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
Card.jsx
import React from 'react';
import styles from './Card.module.css';
function Card({ children }) {
return <div className={styles.card}>{children}</div>;
}
Now, the Card
component will have all the flexbox properties from utils.module.css
applied to it, in addition to its own padding and box-shadow. This is an excellent way to create a reusable set of utility classes.
🛠️ Section 3: Applying Multiple Classes
What if you want to apply a base class and a modifier class dynamically? You can't use composes
for dynamic logic in your CSS. Instead, you combine the classes in your JavaScript.
A simple way is to use a template literal.
// Button.jsx
import React from 'react';
import styles from './Button.module.css';
// Assume Button.module.css has .btn and .primary classes (without composes)
function Button({ isPrimary }) {
const classNames = `${styles.btn} ${isPrimary ? styles.primary : ''}`;
return <button className={classNames}>Click Me</button>;
}
This works, but can get messy. A very popular and recommended utility for this is the classnames
library.
Using the classnames
library:
First, install it: npm install classnames
// Button.jsx with classnames library
import React from 'react';
import styles from './Button.module.css';
import cn from 'classnames';
function Button({ isPrimary, isActive }) {
const classNames = cn(
styles.btn, // Always apply the base .btn class
{
[styles.primary]: isPrimary, // Apply .primary only if isPrimary is true
[styles.active]: isActive, // Apply .active only if isActive is true
}
);
return <button className={classNames}>Click Me</button>;
}
The classnames
library is the standard and cleanest way to conditionally apply multiple classes in React.
🚀 Section 4: Escaping the Local Scope with :global
Sometimes, you need to create a global class from within a CSS Module. This is common when you need to style a third-party component, or if you want to define a style that applies to the body
or html
tag.
The :global
keyword allows you to do this.
GlobalStyles.module.css
/* This class will be local and hashed */
.myLocalClass {
color: blue;
}
/* This class will be global and NOT hashed */
:global(.my-global-class) {
padding: 20px;
font-weight: bold;
}
/* You can also style global elements */
:global(body) {
background-color: #f0f0f0;
}
When you import this file, only myLocalClass
will be a property on the styles
object. The .my-global-class
and body
styles will be injected into the document as-is, without hashing, and will apply globally.
💡 Conclusion & Key Takeaways
You've now explored the advanced capabilities of CSS Modules, moving beyond simple local styles to create sophisticated, maintainable, and reusable styling systems.
Let's summarize the key takeaways:
composes
for Inheritance: Usecomposes: className;
to inherit styles from another class, promoting DRY principles.- Composition is Powerful: You can compose from classes in the same file or from other
.module.css
files. - Use
classnames
for Dynamic Classes: For applying classes conditionally in your JSX, theclassnames
library is the industry standard. :global
for Escaping: Use the:global(...)
syntax when you intentionally need to create a global style that won't be hashed.
Challenge Yourself:
Create a typography.module.css
file with base classes like .heading
and .subheading
. Then, create a Card.module.css
file. In Card.module.css
, create a .cardTitle
class that composes
the .heading
class from typography.module.css
and adds a unique color.
➡️ Next Steps
This concludes our two-part look at CSS Modules. You now have a robust tool for creating scalable and conflict-free CSS. In our next article, we'll look at another popular styling paradigm: "Introduction to CSS-in-JS (Part 1): The concept and benefits. A look at styled-components
."
Thank you for your dedication. Stay curious, and happy coding!
glossary
- Composition: In CSS Modules, the ability for one class to inherit the styles of another class using the
composes
keyword. classnames
: A popular and simple JavaScript utility for conditionally joining class names together.:global
: A keyword in CSS Modules used to declare that a specific class or selector should not be locally scoped and should instead be treated as a global style.